From a82a0eed5e2648faf293290ee694fc7f42a224b2 Mon Sep 17 00:00:00 2001 From: Gerben Oolbekkink Date: Mon, 29 Nov 2021 20:18:14 +0100 Subject: [PATCH 1/6] Generated with JHipster 7.0.0 --- .editorconfig | 23 + .eslintignore | 8 + .eslintrc.json | 79 + .gitattributes | 150 + .gitignore | 153 + .huskyrc | 5 + .jhipster/CustomerDetails.json | 62 + .jhipster/Product.json | 52 + .jhipster/ProductCategory.json | 32 + .jhipster/ProductOrder.json | 45 + .jhipster/ShoppingCart.json | 62 + .lintstagedrc.js | 3 + .npmrc | 1 + .prettierignore | 8 + .prettierrc | 18 + .yo-rc.json | 50 + README.md | 189 + build.gradle | 301 ++ checkstyle.xml | 19 + gradle.properties | 59 + gradle/docker.gradle | 23 + gradle/profile_dev.gradle | 56 + gradle/profile_prod.gradle | 46 + gradle/sonar.gradle | 26 + gradle/war.gradle | 16 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 55616 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradle/zipkin.gradle | 3 + gradlew | 188 + gradlew.bat | 100 + jest.conf.js | 56 + package.json | 178 + postcss.config.js | 3 + settings.gradle | 18 + sonar-project.properties | 30 + src/main/docker/app.yml | 29 + .../grafana/provisioning/dashboards/JVM.json | 3778 +++++++++++++++++ .../provisioning/dashboards/dashboard.yml | 11 + .../provisioning/datasources/datasource.yml | 50 + src/main/docker/jhipster-control-center.yml | 52 + src/main/docker/jib/entrypoint.sh | 4 + src/main/docker/monitoring.yml | 31 + src/main/docker/mysql.yml | 15 + src/main/docker/prometheus/prometheus.yml | 31 + src/main/docker/sonar.yml | 13 + .../adyen/demo/store/ApplicationWebXml.java | 19 + .../adyen/demo/store/GeneratedByJHipster.java | 13 + .../java/com/adyen/demo/store/StoreApp.java | 103 + .../demo/store/aop/logging/LoggingAspect.java | 115 + .../store/config/ApplicationProperties.java | 12 + .../demo/store/config/AsyncConfiguration.java | 46 + .../demo/store/config/CacheConfiguration.java | 86 + .../adyen/demo/store/config/Constants.java | 15 + .../store/config/DatabaseConfiguration.java | 57 + .../config/DateTimeFormatConfiguration.java | 20 + .../store/config/JacksonConfiguration.java | 51 + .../store/config/LiquibaseConfiguration.java | 69 + .../store/config/LocaleConfiguration.java | 26 + .../config/LoggingAspectConfiguration.java | 17 + .../store/config/LoggingConfiguration.java | 47 + .../store/config/SecurityConfiguration.java | 102 + .../StaticResourcesWebConfiguration.java | 51 + .../demo/store/config/WebConfigurer.java | 121 + .../adyen/demo/store/config/package-info.java | 4 + .../store/domain/AbstractAuditingEntity.java | 76 + .../adyen/demo/store/domain/Authority.java | 61 + .../demo/store/domain/CustomerDetails.java | 229 + .../com/adyen/demo/store/domain/Product.java | 194 + .../demo/store/domain/ProductCategory.java | 137 + .../adyen/demo/store/domain/ProductOrder.java | 139 + .../adyen/demo/store/domain/ShoppingCart.java | 233 + .../com/adyen/demo/store/domain/User.java | 231 + .../demo/store/domain/enumeration/Gender.java | 10 + .../store/domain/enumeration/OrderStatus.java | 14 + .../domain/enumeration/PaymentMethod.java | 19 + .../demo/store/domain/enumeration/Size.java | 12 + .../adyen/demo/store/domain/package-info.java | 4 + .../store/repository/AuthorityRepository.java | 9 + .../repository/CustomerDetailsRepository.java | 12 + .../repository/ProductCategoryRepository.java | 12 + .../repository/ProductOrderRepository.java | 12 + .../store/repository/ProductRepository.java | 12 + .../repository/ShoppingCartRepository.java | 12 + .../demo/store/repository/UserRepository.java | 42 + .../demo/store/repository/package-info.java | 4 + .../store/security/AuthoritiesConstants.java | 15 + .../security/DomainUserDetailsService.java | 62 + .../demo/store/security/SecurityUtils.java | 77 + .../security/SpringSecurityAuditorAware.java | 18 + .../security/UserNotActivatedException.java | 19 + .../store/security/jwt/JWTConfigurer.java | 21 + .../demo/store/security/jwt/JWTFilter.java | 47 + .../store/security/jwt/TokenProvider.java | 100 + .../demo/store/security/package-info.java | 4 + .../store/service/CustomerDetailsService.java | 110 + .../service/EmailAlreadyUsedException.java | 10 + .../service/InvalidPasswordException.java | 10 + .../adyen/demo/store/service/MailService.java | 112 + .../store/service/ProductCategoryService.java | 98 + .../store/service/ProductOrderService.java | 96 + .../demo/store/service/ProductService.java | 110 + .../store/service/ShoppingCartService.java | 108 + .../adyen/demo/store/service/UserService.java | 345 ++ .../service/UsernameAlreadyUsedException.java | 10 + .../demo/store/service/dto/AdminUserDTO.java | 193 + .../store/service/dto/PasswordChangeDTO.java | 35 + .../adyen/demo/store/service/dto/UserDTO.java | 48 + .../demo/store/service/dto/package-info.java | 4 + .../demo/store/service/mapper/UserMapper.java | 149 + .../store/service/mapper/package-info.java | 4 + .../demo/store/service/package-info.java | 4 + .../demo/store/web/rest/AccountResource.java | 195 + .../web/rest/ClientForwardController.java | 17 + .../web/rest/CustomerDetailsResource.java | 184 + .../web/rest/ProductCategoryResource.java | 184 + .../store/web/rest/ProductOrderResource.java | 174 + .../demo/store/web/rest/ProductResource.java | 183 + .../store/web/rest/PublicUserResource.java | 65 + .../store/web/rest/ShoppingCartResource.java | 174 + .../store/web/rest/UserJWTController.java | 68 + .../demo/store/web/rest/UserResource.java | 200 + .../rest/errors/BadRequestAlertException.java | 41 + .../errors/EmailAlreadyUsedException.java | 10 + .../store/web/rest/errors/ErrorConstants.java | 17 + .../web/rest/errors/ExceptionTranslator.java | 223 + .../store/web/rest/errors/FieldErrorVM.java | 32 + .../rest/errors/InvalidPasswordException.java | 13 + .../errors/LoginAlreadyUsedException.java | 10 + .../store/web/rest/errors/package-info.java | 6 + .../demo/store/web/rest/package-info.java | 4 + .../store/web/rest/vm/KeyAndPasswordVM.java | 27 + .../adyen/demo/store/web/rest/vm/LoginVM.java | 53 + .../demo/store/web/rest/vm/ManagedUserVM.java | 35 + .../demo/store/web/rest/vm/package-info.java | 4 + src/main/resources/.h2.server.properties | 5 + src/main/resources/banner.txt | 10 + src/main/resources/config/application-dev.yml | 108 + .../resources/config/application-prod.yml | 130 + src/main/resources/config/application-tls.yml | 19 + src/main/resources/config/application.yml | 180 + .../00000000000000_initial_schema.xml | 115 + .../20200424080100_added_entity_Product.xml | 68 + ...80100_added_entity_constraints_Product.xml | 18 + ...424080200_added_entity_ProductCategory.xml | 48 + ...424080300_added_entity_CustomerDetails.xml | 68 + ...ded_entity_constraints_CustomerDetails.xml | 18 + ...200424080400_added_entity_ShoppingCart.xml | 69 + ..._added_entity_constraints_ShoppingCart.xml | 18 + ...200424080500_added_entity_ProductOrder.xml | 56 + ..._added_entity_constraints_ProductOrder.xml | 24 + .../config/liquibase/data/authority.csv | 3 + .../resources/config/liquibase/data/user.csv | 3 + .../config/liquibase/data/user_authority.csv | 4 + .../liquibase/fake-data/blob/hipster.png | Bin 0 -> 7564 bytes .../liquibase/fake-data/customer_details.csv | 3 + .../config/liquibase/fake-data/product.csv | 11 + .../liquibase/fake-data/product_category.csv | 11 + .../liquibase/fake-data/product_order.csv | 11 + .../liquibase/fake-data/shopping_cart.csv | 11 + .../resources/config/liquibase/master.xml | 30 + src/main/resources/i18n/messages.properties | 21 + src/main/resources/logback-spring.xml | 68 + src/main/resources/templates/error.html | 92 + .../templates/mail/activationEmail.html | 20 + .../templates/mail/creationEmail.html | 20 + .../templates/mail/passwordResetEmail.html | 22 + src/main/webapp/404.html | 58 + src/main/webapp/WEB-INF/web.xml | 13 + src/main/webapp/app/_bootstrap-variables.scss | 28 + src/main/webapp/app/app.scss | 312 ++ src/main/webapp/app/app.tsx | 72 + .../app/config/axios-interceptor.spec.ts | 31 + .../webapp/app/config/axios-interceptor.ts | 30 + src/main/webapp/app/config/constants.ts | 24 + src/main/webapp/app/config/dayjs.ts | 11 + src/main/webapp/app/config/devtools.tsx | 11 + .../webapp/app/config/error-middleware.ts | 38 + src/main/webapp/app/config/icon-loader.ts | 73 + .../webapp/app/config/logger-middleware.ts | 13 + .../config/notification-middleware.spec.ts | 190 + .../app/config/notification-middleware.ts | 105 + src/main/webapp/app/config/store.ts | 26 + .../customer-details-delete-dialog.tsx | 63 + .../customer-details-detail.tsx | 77 + .../customer-details-reducer.spec.ts | 304 ++ .../customer-details-update.tsx | 211 + .../customer-details.reducer.ts | 161 + .../customer-details/customer-details.tsx | 202 + .../app/entities/customer-details/index.tsx | 23 + src/main/webapp/app/entities/index.tsx | 28 + .../app/entities/product-category/index.tsx | 23 + .../product-category-delete-dialog.tsx | 63 + .../product-category-detail.tsx | 59 + .../product-category-reducer.spec.ts | 304 ++ .../product-category-update.tsx | 131 + .../product-category.reducer.ts | 161 + .../product-category/product-category.tsx | 182 + .../app/entities/product-order/index.tsx | 23 + .../product-order-delete-dialog.tsx | 63 + .../product-order/product-order-detail.tsx | 63 + .../product-order-reducer.spec.ts | 302 ++ .../product-order/product-order-update.tsx | 185 + .../product-order/product-order.reducer.ts | 156 + .../entities/product-order/product-order.tsx | 108 + .../webapp/app/entities/product/index.tsx | 23 + .../product/product-delete-dialog.tsx | 63 + .../app/entities/product/product-detail.tsx | 86 + .../entities/product/product-reducer.spec.ts | 323 ++ .../app/entities/product/product-update.tsx | 237 ++ .../app/entities/product/product.reducer.ts | 182 + .../webapp/app/entities/product/product.tsx | 218 + .../app/entities/shopping-cart/index.tsx | 23 + .../shopping-cart-delete-dialog.tsx | 63 + .../shopping-cart/shopping-cart-detail.tsx | 81 + .../shopping-cart-reducer.spec.ts | 302 ++ .../shopping-cart/shopping-cart-update.tsx | 228 + .../shopping-cart/shopping-cart.reducer.ts | 156 + .../entities/shopping-cart/shopping-cart.tsx | 122 + src/main/webapp/app/index.tsx | 40 + .../account/activate/activate.reducer.spec.ts | 95 + .../account/activate/activate.reducer.ts | 51 + .../app/modules/account/activate/activate.tsx | 60 + src/main/webapp/app/modules/account/index.tsx | 15 + .../finish/password-reset-finish.tsx | 81 + .../init/password-reset-init.tsx | 58 + .../password-reset/password-reset.reducer.ts | 73 + .../account/password/password.reducer.spec.ts | 108 + .../account/password/password.reducer.ts | 66 + .../app/modules/account/password/password.tsx | 106 + .../account/register/register.reducer.spec.ts | 103 + .../account/register/register.reducer.ts | 58 + .../app/modules/account/register/register.tsx | 119 + .../account/settings/settings.reducer.spec.ts | 116 + .../account/settings/settings.reducer.ts | 70 + .../app/modules/account/settings/settings.tsx | 101 + .../administration.reducer.spec.ts | 273 ++ .../administration/administration.reducer.ts | 149 + .../configuration/configuration.tsx | 120 + .../app/modules/administration/docs/docs.scss | 3 + .../app/modules/administration/docs/docs.tsx | 19 + .../administration/health/health-modal.tsx | 47 + .../modules/administration/health/health.tsx | 97 + .../app/modules/administration/index.tsx | 22 + .../app/modules/administration/logs/logs.tsx | 116 + .../administration/metrics/metrics.tsx | 128 + .../administration/user-management/index.tsx | 22 + .../user-management-delete-dialog.tsx | 57 + .../user-management-detail.tsx | 82 + .../user-management-update.tsx | 193 + .../user-management.reducer.spec.ts | 347 ++ .../user-management.reducer.ts | 175 + .../user-management/user-management.tsx | 213 + src/main/webapp/app/modules/home/home.scss | 10 + src/main/webapp/app/modules/home/home.tsx | 97 + .../webapp/app/modules/login/login-modal.tsx | 88 + src/main/webapp/app/modules/login/login.tsx | 44 + src/main/webapp/app/modules/login/logout.tsx | 41 + src/main/webapp/app/routes.tsx | 48 + src/main/webapp/app/setup-tests.ts | 3 + src/main/webapp/app/shared/DurationFormat.tsx | 28 + .../app/shared/auth/private-route.spec.tsx | 81 + .../webapp/app/shared/auth/private-route.tsx | 84 + .../error/error-boundary-route.spec.tsx | 33 + .../app/shared/error/error-boundary-route.tsx | 17 + .../app/shared/error/error-boundary.spec.tsx | 30 + .../app/shared/error/error-boundary.tsx | 44 + .../app/shared/error/page-not-found.tsx | 15 + .../app/shared/layout/footer/footer.scss | 3 + .../app/shared/layout/footer/footer.tsx | 17 + .../layout/header/header-components.tsx | 30 + .../app/shared/layout/header/header.scss | 113 + .../app/shared/layout/header/header.spec.tsx | 112 + .../app/shared/layout/header/header.tsx | 56 + .../app/shared/layout/menus/account.spec.tsx | 56 + .../app/shared/layout/menus/account.tsx | 37 + .../webapp/app/shared/layout/menus/admin.tsx | 49 + .../app/shared/layout/menus/entities.tsx | 25 + .../webapp/app/shared/layout/menus/index.ts | 3 + .../shared/layout/menus/menu-components.tsx | 16 + .../app/shared/layout/menus/menu-item.tsx | 24 + .../password/password-strength-bar.scss | 23 + .../layout/password/password-strength-bar.tsx | 73 + .../shared/model/customer-details.model.ts | 17 + .../shared/model/enumerations/gender.model.ts | 7 + .../model/enumerations/order-status.model.ts | 15 + .../enumerations/payment-method.model.ts | 5 + .../shared/model/enumerations/size.model.ts | 11 + .../shared/model/product-category.model.ts | 10 + .../app/shared/model/product-order.model.ts | 12 + .../webapp/app/shared/model/product.model.ts | 15 + .../app/shared/model/shopping-cart.model.ts | 19 + .../webapp/app/shared/model/user.model.ts | 31 + .../app/shared/reducers/action-type.util.ts | 17 + .../reducers/application-profile.spec.ts | 76 + .../shared/reducers/application-profile.ts | 36 + .../shared/reducers/authentication.spec.ts | 252 ++ .../app/shared/reducers/authentication.ts | 150 + src/main/webapp/app/shared/reducers/index.ts | 74 + src/main/webapp/app/shared/util/date-utils.ts | 9 + .../app/shared/util/entity-utils.spec.ts | 56 + .../webapp/app/shared/util/entity-utils.ts | 37 + .../app/shared/util/pagination.constants.ts | 1 + src/main/webapp/app/typings.d.ts | 4 + src/main/webapp/content/css/loading.css | 152 + .../images/jhipster_family_member_0.svg | 1 + .../jhipster_family_member_0_head-192.png | Bin 0 -> 13439 bytes .../jhipster_family_member_0_head-256.png | Bin 0 -> 7037 bytes .../jhipster_family_member_0_head-384.png | Bin 0 -> 10350 bytes .../jhipster_family_member_0_head-512.png | Bin 0 -> 11431 bytes .../images/jhipster_family_member_1.svg | 1 + .../jhipster_family_member_1_head-192.png | Bin 0 -> 7046 bytes .../jhipster_family_member_1_head-256.png | Bin 0 -> 9505 bytes .../jhipster_family_member_1_head-384.png | Bin 0 -> 15054 bytes .../jhipster_family_member_1_head-512.png | Bin 0 -> 16456 bytes .../images/jhipster_family_member_2.svg | 1 + .../jhipster_family_member_2_head-192.png | Bin 0 -> 5423 bytes .../jhipster_family_member_2_head-256.png | Bin 0 -> 6687 bytes .../jhipster_family_member_2_head-384.png | Bin 0 -> 9682 bytes .../jhipster_family_member_2_head-512.png | Bin 0 -> 10514 bytes .../images/jhipster_family_member_3.svg | 1 + .../jhipster_family_member_3_head-192.png | Bin 0 -> 6148 bytes .../jhipster_family_member_3_head-256.png | Bin 0 -> 8028 bytes .../jhipster_family_member_3_head-384.png | Bin 0 -> 11998 bytes .../jhipster_family_member_3_head-512.png | Bin 0 -> 13555 bytes .../webapp/content/images/logo-jhipster.png | Bin 0 -> 605 bytes src/main/webapp/favicon.ico | Bin 0 -> 1574 bytes src/main/webapp/index.html | 138 + src/main/webapp/manifest.webapp | 31 + src/main/webapp/robots.txt | 11 + .../swagger-ui/dist/images/throbber.gif | Bin 0 -> 9257 bytes src/main/webapp/swagger-ui/index.html | 81 + .../java/com/adyen/demo/store/ArchTest.java | 29 + .../com/adyen/demo/store/IntegrationTest.java | 17 + .../store/config/NoOpMailConfiguration.java | 25 + .../StaticResourcesWebConfigurerTest.java | 76 + .../demo/store/config/WebConfigurerTest.java | 150 + .../config/WebConfigurerTestController.java | 14 + .../config/timezone/HibernateTimeZoneIT.java | 162 + .../store/domain/CustomerDetailsTest.java | 23 + .../store/domain/ProductCategoryTest.java | 23 + .../demo/store/domain/ProductOrderTest.java | 23 + .../adyen/demo/store/domain/ProductTest.java | 23 + .../demo/store/domain/ShoppingCartTest.java | 23 + .../repository/timezone/DateTimeWrapper.java | 132 + .../timezone/DateTimeWrapperRepository.java | 10 + .../security/DomainUserDetailsServiceIT.java | 111 + .../store/security/SecurityUtilsUnitTest.java | 77 + .../store/security/jwt/JWTFilterTest.java | 112 + .../store/security/jwt/TokenProviderTest.java | 132 + .../demo/store/service/MailServiceIT.java | 243 ++ .../demo/store/service/UserServiceIT.java | 185 + .../store/service/mapper/UserMapperTest.java | 132 + .../store/web/rest/AccountResourceIT.java | 762 ++++ .../web/rest/ClientForwardControllerTest.java | 58 + .../web/rest/CustomerDetailsResourceIT.java | 552 +++ .../web/rest/ProductCategoryResourceIT.java | 402 ++ .../web/rest/ProductOrderResourceIT.java | 453 ++ .../store/web/rest/ProductResourceIT.java | 510 +++ .../store/web/rest/PublicUserResourceIT.java | 99 + .../web/rest/ShoppingCartResourceIT.java | 537 +++ .../adyen/demo/store/web/rest/TestUtil.java | 206 + .../store/web/rest/UserJWTControllerIT.java | 98 + .../demo/store/web/rest/UserResourceIT.java | 589 +++ .../web/rest/WithUnauthenticatedMockUser.java | 23 + .../rest/errors/ExceptionTranslatorIT.java | 118 + .../ExceptionTranslatorTestController.java | 66 + .../config/application-testcontainers.yml | 27 + src/test/resources/config/application.yml | 112 + .../resources/i18n/messages_en.properties | 1 + src/test/resources/logback.xml | 48 + .../resources/templates/mail/testEmail.html | 1 + tsconfig.json | 28 + tsconfig.test.json | 7 + webpack/logo-jhipster.png | Bin 0 -> 3326 bytes webpack/utils.js | 39 + webpack/webpack.common.js | 132 + webpack/webpack.dev.js | 104 + webpack/webpack.prod.js | 99 + 378 files changed, 32322 insertions(+) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .huskyrc create mode 100644 .jhipster/CustomerDetails.json create mode 100644 .jhipster/Product.json create mode 100644 .jhipster/ProductCategory.json create mode 100644 .jhipster/ProductOrder.json create mode 100644 .jhipster/ShoppingCart.json create mode 100644 .lintstagedrc.js create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .yo-rc.json create mode 100644 README.md create mode 100644 build.gradle create mode 100644 checkstyle.xml create mode 100644 gradle.properties create mode 100644 gradle/docker.gradle create mode 100644 gradle/profile_dev.gradle create mode 100644 gradle/profile_prod.gradle create mode 100644 gradle/sonar.gradle create mode 100644 gradle/war.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradle/zipkin.gradle create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 jest.conf.js create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 settings.gradle create mode 100644 sonar-project.properties create mode 100644 src/main/docker/app.yml create mode 100644 src/main/docker/grafana/provisioning/dashboards/JVM.json create mode 100644 src/main/docker/grafana/provisioning/dashboards/dashboard.yml create mode 100644 src/main/docker/grafana/provisioning/datasources/datasource.yml create mode 100644 src/main/docker/jhipster-control-center.yml create mode 100644 src/main/docker/jib/entrypoint.sh create mode 100644 src/main/docker/monitoring.yml create mode 100644 src/main/docker/mysql.yml create mode 100644 src/main/docker/prometheus/prometheus.yml create mode 100644 src/main/docker/sonar.yml create mode 100644 src/main/java/com/adyen/demo/store/ApplicationWebXml.java create mode 100644 src/main/java/com/adyen/demo/store/GeneratedByJHipster.java create mode 100644 src/main/java/com/adyen/demo/store/StoreApp.java create mode 100644 src/main/java/com/adyen/demo/store/aop/logging/LoggingAspect.java create mode 100644 src/main/java/com/adyen/demo/store/config/ApplicationProperties.java create mode 100644 src/main/java/com/adyen/demo/store/config/AsyncConfiguration.java create mode 100644 src/main/java/com/adyen/demo/store/config/CacheConfiguration.java create mode 100644 src/main/java/com/adyen/demo/store/config/Constants.java create mode 100644 src/main/java/com/adyen/demo/store/config/DatabaseConfiguration.java create mode 100644 src/main/java/com/adyen/demo/store/config/DateTimeFormatConfiguration.java create mode 100644 src/main/java/com/adyen/demo/store/config/JacksonConfiguration.java create mode 100644 src/main/java/com/adyen/demo/store/config/LiquibaseConfiguration.java create mode 100644 src/main/java/com/adyen/demo/store/config/LocaleConfiguration.java create mode 100644 src/main/java/com/adyen/demo/store/config/LoggingAspectConfiguration.java create mode 100644 src/main/java/com/adyen/demo/store/config/LoggingConfiguration.java create mode 100644 src/main/java/com/adyen/demo/store/config/SecurityConfiguration.java create mode 100644 src/main/java/com/adyen/demo/store/config/StaticResourcesWebConfiguration.java create mode 100644 src/main/java/com/adyen/demo/store/config/WebConfigurer.java create mode 100644 src/main/java/com/adyen/demo/store/config/package-info.java create mode 100644 src/main/java/com/adyen/demo/store/domain/AbstractAuditingEntity.java create mode 100644 src/main/java/com/adyen/demo/store/domain/Authority.java create mode 100644 src/main/java/com/adyen/demo/store/domain/CustomerDetails.java create mode 100644 src/main/java/com/adyen/demo/store/domain/Product.java create mode 100644 src/main/java/com/adyen/demo/store/domain/ProductCategory.java create mode 100644 src/main/java/com/adyen/demo/store/domain/ProductOrder.java create mode 100644 src/main/java/com/adyen/demo/store/domain/ShoppingCart.java create mode 100644 src/main/java/com/adyen/demo/store/domain/User.java create mode 100644 src/main/java/com/adyen/demo/store/domain/enumeration/Gender.java create mode 100644 src/main/java/com/adyen/demo/store/domain/enumeration/OrderStatus.java create mode 100644 src/main/java/com/adyen/demo/store/domain/enumeration/PaymentMethod.java create mode 100644 src/main/java/com/adyen/demo/store/domain/enumeration/Size.java create mode 100644 src/main/java/com/adyen/demo/store/domain/package-info.java create mode 100644 src/main/java/com/adyen/demo/store/repository/AuthorityRepository.java create mode 100644 src/main/java/com/adyen/demo/store/repository/CustomerDetailsRepository.java create mode 100644 src/main/java/com/adyen/demo/store/repository/ProductCategoryRepository.java create mode 100644 src/main/java/com/adyen/demo/store/repository/ProductOrderRepository.java create mode 100644 src/main/java/com/adyen/demo/store/repository/ProductRepository.java create mode 100644 src/main/java/com/adyen/demo/store/repository/ShoppingCartRepository.java create mode 100644 src/main/java/com/adyen/demo/store/repository/UserRepository.java create mode 100644 src/main/java/com/adyen/demo/store/repository/package-info.java create mode 100644 src/main/java/com/adyen/demo/store/security/AuthoritiesConstants.java create mode 100644 src/main/java/com/adyen/demo/store/security/DomainUserDetailsService.java create mode 100644 src/main/java/com/adyen/demo/store/security/SecurityUtils.java create mode 100644 src/main/java/com/adyen/demo/store/security/SpringSecurityAuditorAware.java create mode 100644 src/main/java/com/adyen/demo/store/security/UserNotActivatedException.java create mode 100644 src/main/java/com/adyen/demo/store/security/jwt/JWTConfigurer.java create mode 100644 src/main/java/com/adyen/demo/store/security/jwt/JWTFilter.java create mode 100644 src/main/java/com/adyen/demo/store/security/jwt/TokenProvider.java create mode 100644 src/main/java/com/adyen/demo/store/security/package-info.java create mode 100644 src/main/java/com/adyen/demo/store/service/CustomerDetailsService.java create mode 100644 src/main/java/com/adyen/demo/store/service/EmailAlreadyUsedException.java create mode 100644 src/main/java/com/adyen/demo/store/service/InvalidPasswordException.java create mode 100644 src/main/java/com/adyen/demo/store/service/MailService.java create mode 100644 src/main/java/com/adyen/demo/store/service/ProductCategoryService.java create mode 100644 src/main/java/com/adyen/demo/store/service/ProductOrderService.java create mode 100644 src/main/java/com/adyen/demo/store/service/ProductService.java create mode 100644 src/main/java/com/adyen/demo/store/service/ShoppingCartService.java create mode 100644 src/main/java/com/adyen/demo/store/service/UserService.java create mode 100644 src/main/java/com/adyen/demo/store/service/UsernameAlreadyUsedException.java create mode 100644 src/main/java/com/adyen/demo/store/service/dto/AdminUserDTO.java create mode 100644 src/main/java/com/adyen/demo/store/service/dto/PasswordChangeDTO.java create mode 100644 src/main/java/com/adyen/demo/store/service/dto/UserDTO.java create mode 100644 src/main/java/com/adyen/demo/store/service/dto/package-info.java create mode 100644 src/main/java/com/adyen/demo/store/service/mapper/UserMapper.java create mode 100644 src/main/java/com/adyen/demo/store/service/mapper/package-info.java create mode 100644 src/main/java/com/adyen/demo/store/service/package-info.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/AccountResource.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/ClientForwardController.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/CustomerDetailsResource.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/ProductCategoryResource.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/ProductOrderResource.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/ProductResource.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/PublicUserResource.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/ShoppingCartResource.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/UserJWTController.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/UserResource.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/errors/BadRequestAlertException.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/errors/EmailAlreadyUsedException.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/errors/ErrorConstants.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/errors/ExceptionTranslator.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/errors/FieldErrorVM.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/errors/InvalidPasswordException.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/errors/LoginAlreadyUsedException.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/errors/package-info.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/package-info.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/vm/KeyAndPasswordVM.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/vm/LoginVM.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/vm/ManagedUserVM.java create mode 100644 src/main/java/com/adyen/demo/store/web/rest/vm/package-info.java create mode 100644 src/main/resources/.h2.server.properties create mode 100644 src/main/resources/banner.txt create mode 100644 src/main/resources/config/application-dev.yml create mode 100644 src/main/resources/config/application-prod.yml create mode 100644 src/main/resources/config/application-tls.yml create mode 100644 src/main/resources/config/application.yml create mode 100644 src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml create mode 100644 src/main/resources/config/liquibase/changelog/20200424080100_added_entity_Product.xml create mode 100644 src/main/resources/config/liquibase/changelog/20200424080100_added_entity_constraints_Product.xml create mode 100644 src/main/resources/config/liquibase/changelog/20200424080200_added_entity_ProductCategory.xml create mode 100644 src/main/resources/config/liquibase/changelog/20200424080300_added_entity_CustomerDetails.xml create mode 100644 src/main/resources/config/liquibase/changelog/20200424080300_added_entity_constraints_CustomerDetails.xml create mode 100644 src/main/resources/config/liquibase/changelog/20200424080400_added_entity_ShoppingCart.xml create mode 100644 src/main/resources/config/liquibase/changelog/20200424080400_added_entity_constraints_ShoppingCart.xml create mode 100644 src/main/resources/config/liquibase/changelog/20200424080500_added_entity_ProductOrder.xml create mode 100644 src/main/resources/config/liquibase/changelog/20200424080500_added_entity_constraints_ProductOrder.xml create mode 100644 src/main/resources/config/liquibase/data/authority.csv create mode 100644 src/main/resources/config/liquibase/data/user.csv create mode 100644 src/main/resources/config/liquibase/data/user_authority.csv create mode 100644 src/main/resources/config/liquibase/fake-data/blob/hipster.png create mode 100644 src/main/resources/config/liquibase/fake-data/customer_details.csv create mode 100644 src/main/resources/config/liquibase/fake-data/product.csv create mode 100644 src/main/resources/config/liquibase/fake-data/product_category.csv create mode 100644 src/main/resources/config/liquibase/fake-data/product_order.csv create mode 100644 src/main/resources/config/liquibase/fake-data/shopping_cart.csv create mode 100644 src/main/resources/config/liquibase/master.xml create mode 100644 src/main/resources/i18n/messages.properties create mode 100644 src/main/resources/logback-spring.xml create mode 100644 src/main/resources/templates/error.html create mode 100644 src/main/resources/templates/mail/activationEmail.html create mode 100644 src/main/resources/templates/mail/creationEmail.html create mode 100644 src/main/resources/templates/mail/passwordResetEmail.html create mode 100644 src/main/webapp/404.html create mode 100644 src/main/webapp/WEB-INF/web.xml create mode 100644 src/main/webapp/app/_bootstrap-variables.scss create mode 100644 src/main/webapp/app/app.scss create mode 100644 src/main/webapp/app/app.tsx create mode 100644 src/main/webapp/app/config/axios-interceptor.spec.ts create mode 100644 src/main/webapp/app/config/axios-interceptor.ts create mode 100644 src/main/webapp/app/config/constants.ts create mode 100644 src/main/webapp/app/config/dayjs.ts create mode 100644 src/main/webapp/app/config/devtools.tsx create mode 100644 src/main/webapp/app/config/error-middleware.ts create mode 100644 src/main/webapp/app/config/icon-loader.ts create mode 100644 src/main/webapp/app/config/logger-middleware.ts create mode 100644 src/main/webapp/app/config/notification-middleware.spec.ts create mode 100644 src/main/webapp/app/config/notification-middleware.ts create mode 100644 src/main/webapp/app/config/store.ts create mode 100644 src/main/webapp/app/entities/customer-details/customer-details-delete-dialog.tsx create mode 100644 src/main/webapp/app/entities/customer-details/customer-details-detail.tsx create mode 100644 src/main/webapp/app/entities/customer-details/customer-details-reducer.spec.ts create mode 100644 src/main/webapp/app/entities/customer-details/customer-details-update.tsx create mode 100644 src/main/webapp/app/entities/customer-details/customer-details.reducer.ts create mode 100644 src/main/webapp/app/entities/customer-details/customer-details.tsx create mode 100644 src/main/webapp/app/entities/customer-details/index.tsx create mode 100644 src/main/webapp/app/entities/index.tsx create mode 100644 src/main/webapp/app/entities/product-category/index.tsx create mode 100644 src/main/webapp/app/entities/product-category/product-category-delete-dialog.tsx create mode 100644 src/main/webapp/app/entities/product-category/product-category-detail.tsx create mode 100644 src/main/webapp/app/entities/product-category/product-category-reducer.spec.ts create mode 100644 src/main/webapp/app/entities/product-category/product-category-update.tsx create mode 100644 src/main/webapp/app/entities/product-category/product-category.reducer.ts create mode 100644 src/main/webapp/app/entities/product-category/product-category.tsx create mode 100644 src/main/webapp/app/entities/product-order/index.tsx create mode 100644 src/main/webapp/app/entities/product-order/product-order-delete-dialog.tsx create mode 100644 src/main/webapp/app/entities/product-order/product-order-detail.tsx create mode 100644 src/main/webapp/app/entities/product-order/product-order-reducer.spec.ts create mode 100644 src/main/webapp/app/entities/product-order/product-order-update.tsx create mode 100644 src/main/webapp/app/entities/product-order/product-order.reducer.ts create mode 100644 src/main/webapp/app/entities/product-order/product-order.tsx create mode 100644 src/main/webapp/app/entities/product/index.tsx create mode 100644 src/main/webapp/app/entities/product/product-delete-dialog.tsx create mode 100644 src/main/webapp/app/entities/product/product-detail.tsx create mode 100644 src/main/webapp/app/entities/product/product-reducer.spec.ts create mode 100644 src/main/webapp/app/entities/product/product-update.tsx create mode 100644 src/main/webapp/app/entities/product/product.reducer.ts create mode 100644 src/main/webapp/app/entities/product/product.tsx create mode 100644 src/main/webapp/app/entities/shopping-cart/index.tsx create mode 100644 src/main/webapp/app/entities/shopping-cart/shopping-cart-delete-dialog.tsx create mode 100644 src/main/webapp/app/entities/shopping-cart/shopping-cart-detail.tsx create mode 100644 src/main/webapp/app/entities/shopping-cart/shopping-cart-reducer.spec.ts create mode 100644 src/main/webapp/app/entities/shopping-cart/shopping-cart-update.tsx create mode 100644 src/main/webapp/app/entities/shopping-cart/shopping-cart.reducer.ts create mode 100644 src/main/webapp/app/entities/shopping-cart/shopping-cart.tsx create mode 100644 src/main/webapp/app/index.tsx create mode 100644 src/main/webapp/app/modules/account/activate/activate.reducer.spec.ts create mode 100644 src/main/webapp/app/modules/account/activate/activate.reducer.ts create mode 100644 src/main/webapp/app/modules/account/activate/activate.tsx create mode 100644 src/main/webapp/app/modules/account/index.tsx create mode 100644 src/main/webapp/app/modules/account/password-reset/finish/password-reset-finish.tsx create mode 100644 src/main/webapp/app/modules/account/password-reset/init/password-reset-init.tsx create mode 100644 src/main/webapp/app/modules/account/password-reset/password-reset.reducer.ts create mode 100644 src/main/webapp/app/modules/account/password/password.reducer.spec.ts create mode 100644 src/main/webapp/app/modules/account/password/password.reducer.ts create mode 100644 src/main/webapp/app/modules/account/password/password.tsx create mode 100644 src/main/webapp/app/modules/account/register/register.reducer.spec.ts create mode 100644 src/main/webapp/app/modules/account/register/register.reducer.ts create mode 100644 src/main/webapp/app/modules/account/register/register.tsx create mode 100644 src/main/webapp/app/modules/account/settings/settings.reducer.spec.ts create mode 100644 src/main/webapp/app/modules/account/settings/settings.reducer.ts create mode 100644 src/main/webapp/app/modules/account/settings/settings.tsx create mode 100644 src/main/webapp/app/modules/administration/administration.reducer.spec.ts create mode 100644 src/main/webapp/app/modules/administration/administration.reducer.ts create mode 100644 src/main/webapp/app/modules/administration/configuration/configuration.tsx create mode 100644 src/main/webapp/app/modules/administration/docs/docs.scss create mode 100644 src/main/webapp/app/modules/administration/docs/docs.tsx create mode 100644 src/main/webapp/app/modules/administration/health/health-modal.tsx create mode 100644 src/main/webapp/app/modules/administration/health/health.tsx create mode 100644 src/main/webapp/app/modules/administration/index.tsx create mode 100644 src/main/webapp/app/modules/administration/logs/logs.tsx create mode 100644 src/main/webapp/app/modules/administration/metrics/metrics.tsx create mode 100644 src/main/webapp/app/modules/administration/user-management/index.tsx create mode 100644 src/main/webapp/app/modules/administration/user-management/user-management-delete-dialog.tsx create mode 100644 src/main/webapp/app/modules/administration/user-management/user-management-detail.tsx create mode 100644 src/main/webapp/app/modules/administration/user-management/user-management-update.tsx create mode 100644 src/main/webapp/app/modules/administration/user-management/user-management.reducer.spec.ts create mode 100644 src/main/webapp/app/modules/administration/user-management/user-management.reducer.ts create mode 100644 src/main/webapp/app/modules/administration/user-management/user-management.tsx create mode 100644 src/main/webapp/app/modules/home/home.scss create mode 100644 src/main/webapp/app/modules/home/home.tsx create mode 100644 src/main/webapp/app/modules/login/login-modal.tsx create mode 100644 src/main/webapp/app/modules/login/login.tsx create mode 100644 src/main/webapp/app/modules/login/logout.tsx create mode 100644 src/main/webapp/app/routes.tsx create mode 100644 src/main/webapp/app/setup-tests.ts create mode 100644 src/main/webapp/app/shared/DurationFormat.tsx create mode 100644 src/main/webapp/app/shared/auth/private-route.spec.tsx create mode 100644 src/main/webapp/app/shared/auth/private-route.tsx create mode 100644 src/main/webapp/app/shared/error/error-boundary-route.spec.tsx create mode 100644 src/main/webapp/app/shared/error/error-boundary-route.tsx create mode 100644 src/main/webapp/app/shared/error/error-boundary.spec.tsx create mode 100644 src/main/webapp/app/shared/error/error-boundary.tsx create mode 100644 src/main/webapp/app/shared/error/page-not-found.tsx create mode 100644 src/main/webapp/app/shared/layout/footer/footer.scss create mode 100644 src/main/webapp/app/shared/layout/footer/footer.tsx create mode 100644 src/main/webapp/app/shared/layout/header/header-components.tsx create mode 100644 src/main/webapp/app/shared/layout/header/header.scss create mode 100644 src/main/webapp/app/shared/layout/header/header.spec.tsx create mode 100644 src/main/webapp/app/shared/layout/header/header.tsx create mode 100644 src/main/webapp/app/shared/layout/menus/account.spec.tsx create mode 100644 src/main/webapp/app/shared/layout/menus/account.tsx create mode 100644 src/main/webapp/app/shared/layout/menus/admin.tsx create mode 100644 src/main/webapp/app/shared/layout/menus/entities.tsx create mode 100644 src/main/webapp/app/shared/layout/menus/index.ts create mode 100644 src/main/webapp/app/shared/layout/menus/menu-components.tsx create mode 100644 src/main/webapp/app/shared/layout/menus/menu-item.tsx create mode 100644 src/main/webapp/app/shared/layout/password/password-strength-bar.scss create mode 100644 src/main/webapp/app/shared/layout/password/password-strength-bar.tsx create mode 100644 src/main/webapp/app/shared/model/customer-details.model.ts create mode 100644 src/main/webapp/app/shared/model/enumerations/gender.model.ts create mode 100644 src/main/webapp/app/shared/model/enumerations/order-status.model.ts create mode 100644 src/main/webapp/app/shared/model/enumerations/payment-method.model.ts create mode 100644 src/main/webapp/app/shared/model/enumerations/size.model.ts create mode 100644 src/main/webapp/app/shared/model/product-category.model.ts create mode 100644 src/main/webapp/app/shared/model/product-order.model.ts create mode 100644 src/main/webapp/app/shared/model/product.model.ts create mode 100644 src/main/webapp/app/shared/model/shopping-cart.model.ts create mode 100644 src/main/webapp/app/shared/model/user.model.ts create mode 100644 src/main/webapp/app/shared/reducers/action-type.util.ts create mode 100644 src/main/webapp/app/shared/reducers/application-profile.spec.ts create mode 100644 src/main/webapp/app/shared/reducers/application-profile.ts create mode 100644 src/main/webapp/app/shared/reducers/authentication.spec.ts create mode 100644 src/main/webapp/app/shared/reducers/authentication.ts create mode 100644 src/main/webapp/app/shared/reducers/index.ts create mode 100644 src/main/webapp/app/shared/util/date-utils.ts create mode 100644 src/main/webapp/app/shared/util/entity-utils.spec.ts create mode 100644 src/main/webapp/app/shared/util/entity-utils.ts create mode 100644 src/main/webapp/app/shared/util/pagination.constants.ts create mode 100644 src/main/webapp/app/typings.d.ts create mode 100644 src/main/webapp/content/css/loading.css create mode 100644 src/main/webapp/content/images/jhipster_family_member_0.svg create mode 100644 src/main/webapp/content/images/jhipster_family_member_0_head-192.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_0_head-256.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_0_head-384.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_0_head-512.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_1.svg create mode 100644 src/main/webapp/content/images/jhipster_family_member_1_head-192.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_1_head-256.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_1_head-384.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_1_head-512.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_2.svg create mode 100644 src/main/webapp/content/images/jhipster_family_member_2_head-192.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_2_head-256.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_2_head-384.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_2_head-512.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_3.svg create mode 100644 src/main/webapp/content/images/jhipster_family_member_3_head-192.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_3_head-256.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_3_head-384.png create mode 100644 src/main/webapp/content/images/jhipster_family_member_3_head-512.png create mode 100644 src/main/webapp/content/images/logo-jhipster.png create mode 100644 src/main/webapp/favicon.ico create mode 100644 src/main/webapp/index.html create mode 100644 src/main/webapp/manifest.webapp create mode 100644 src/main/webapp/robots.txt create mode 100644 src/main/webapp/swagger-ui/dist/images/throbber.gif create mode 100644 src/main/webapp/swagger-ui/index.html create mode 100644 src/test/java/com/adyen/demo/store/ArchTest.java create mode 100644 src/test/java/com/adyen/demo/store/IntegrationTest.java create mode 100644 src/test/java/com/adyen/demo/store/config/NoOpMailConfiguration.java create mode 100644 src/test/java/com/adyen/demo/store/config/StaticResourcesWebConfigurerTest.java create mode 100644 src/test/java/com/adyen/demo/store/config/WebConfigurerTest.java create mode 100644 src/test/java/com/adyen/demo/store/config/WebConfigurerTestController.java create mode 100644 src/test/java/com/adyen/demo/store/config/timezone/HibernateTimeZoneIT.java create mode 100644 src/test/java/com/adyen/demo/store/domain/CustomerDetailsTest.java create mode 100644 src/test/java/com/adyen/demo/store/domain/ProductCategoryTest.java create mode 100644 src/test/java/com/adyen/demo/store/domain/ProductOrderTest.java create mode 100644 src/test/java/com/adyen/demo/store/domain/ProductTest.java create mode 100644 src/test/java/com/adyen/demo/store/domain/ShoppingCartTest.java create mode 100644 src/test/java/com/adyen/demo/store/repository/timezone/DateTimeWrapper.java create mode 100644 src/test/java/com/adyen/demo/store/repository/timezone/DateTimeWrapperRepository.java create mode 100644 src/test/java/com/adyen/demo/store/security/DomainUserDetailsServiceIT.java create mode 100644 src/test/java/com/adyen/demo/store/security/SecurityUtilsUnitTest.java create mode 100644 src/test/java/com/adyen/demo/store/security/jwt/JWTFilterTest.java create mode 100644 src/test/java/com/adyen/demo/store/security/jwt/TokenProviderTest.java create mode 100644 src/test/java/com/adyen/demo/store/service/MailServiceIT.java create mode 100644 src/test/java/com/adyen/demo/store/service/UserServiceIT.java create mode 100644 src/test/java/com/adyen/demo/store/service/mapper/UserMapperTest.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/AccountResourceIT.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/ClientForwardControllerTest.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/CustomerDetailsResourceIT.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/ProductCategoryResourceIT.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/ProductOrderResourceIT.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/ProductResourceIT.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/PublicUserResourceIT.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/ShoppingCartResourceIT.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/TestUtil.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/UserJWTControllerIT.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/UserResourceIT.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/WithUnauthenticatedMockUser.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/errors/ExceptionTranslatorIT.java create mode 100644 src/test/java/com/adyen/demo/store/web/rest/errors/ExceptionTranslatorTestController.java create mode 100644 src/test/resources/config/application-testcontainers.yml create mode 100644 src/test/resources/config/application.yml create mode 100644 src/test/resources/i18n/messages_en.properties create mode 100644 src/test/resources/logback.xml create mode 100644 src/test/resources/templates/mail/testEmail.html create mode 100644 tsconfig.json create mode 100644 tsconfig.test.json create mode 100644 webpack/logo-jhipster.png create mode 100644 webpack/utils.js create mode 100644 webpack/webpack.common.js create mode 100644 webpack/webpack.dev.js create mode 100644 webpack/webpack.prod.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c2fa6a2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +# Change these settings to your own preference +indent_style = space +indent_size = 4 + +[*.{ts,tsx,js,jsx,json,css,scss,yml,html,vue}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..0f4bda1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,8 @@ +node_modules/ +src/main/docker/ +jest.conf.js +webpack/ +target/ +build/ +node/ +postcss.config.js diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..16f4b2f --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,79 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": [ + "plugin:react/recommended", + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "prettier", + "eslint-config-prettier" + ], + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + }, + "project": "./tsconfig.json" + }, + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + "@typescript-eslint/member-ordering": [ + "error", + { + "default": ["static-field", "instance-field", "constructor", "static-method", "instance-method"] + } + ], + "@typescript-eslint/no-parameter-properties": ["warn", { "allows": ["public", "private", "protected"] }], + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/explicit-member-accessibility": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "@typescript-eslint/restrict-plus-operands": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/ban-types": [ + "error", + { + "types": { + "Object": "Use {} instead." + } + } + ], + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/unbound-method": "off", + "@typescript-eslint/array-type": "off", + "@typescript-eslint/no-shadow": "error", + "spaced-comment": ["warn", "always"], + "guard-for-in": "error", + "no-labels": "error", + "no-caller": "error", + "no-bitwise": "error", + "no-console": ["error", { "allow": ["warn", "error"] }], + "no-new-wrappers": "error", + "no-eval": "error", + "no-new": "error", + "no-var": "error", + "radix": "error", + "eqeqeq": ["error", "always", { "null": "ignore" }], + "prefer-const": "error", + "object-shorthand": ["error", "always", { "avoidExplicitReturnArrows": true }], + "default-case": "error", + "complexity": ["error", 40], + "no-invalid-this": "off", + "react/prop-types": "off", + "react/display-name": "off" + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ca61722 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,150 @@ +# This file is inspired by https://github.com/alexkaratarakis/gitattributes +# +# Auto detect text files and perform LF normalization +# http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ +* text=auto + +# The above will handle all files NOT found below +# These files are text and should be normalized (Convert crlf => lf) + +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf +*.coffee text +*.css text +*.cql text +*.df text +*.ejs text +*.html text +*.java text +*.js text +*.json text +*.less text +*.properties text +*.sass text +*.scss text +*.sh text eol=lf +*.sql text +*.txt text +*.ts text +*.xml text +*.yaml text +*.yml text + +# Documents +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain +*.markdown text +*.md text +*.adoc text +*.textile text +*.mustache text +*.csv text +*.tab text +*.tsv text +*.txt text +AUTHORS text +CHANGELOG text +CHANGES text +CONTRIBUTING text +COPYING text +copyright text +*COPYRIGHT* text +INSTALL text +license text +LICENSE text +NEWS text +readme text +*README* text +TODO text + +# Graphics +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.tif binary +*.tiff binary +*.ico binary +# SVG treated as an asset (binary) by default. If you want to treat it as text, +# comment-out the following line and uncomment the line after. +*.svg binary +#*.svg text +*.eps binary + +# These files are binary and should be left untouched +# (binary is a macro for -text -diff) +*.class binary +*.jar binary +*.war binary + +## LINTERS +.csslintrc text +.eslintrc text +.jscsrc text +.jshintrc text +.jshintignore text +.stylelintrc text + +## CONFIGS +*.conf text +*.config text +.editorconfig text +.gitattributes text +.gitconfig text +.gitignore text +.htaccess text +*.npmignore text + +## HEROKU +Procfile text +.slugignore text + +## AUDIO +*.kar binary +*.m4a binary +*.mid binary +*.midi binary +*.mp3 binary +*.ogg binary +*.ra binary + +## VIDEO +*.3gpp binary +*.3gp binary +*.as binary +*.asf binary +*.asx binary +*.fla binary +*.flv binary +*.m4v binary +*.mng binary +*.mov binary +*.mp4 binary +*.mpeg binary +*.mpg binary +*.swc binary +*.swf binary +*.webm binary + +## ARCHIVES +*.7z binary +*.gz binary +*.rar binary +*.tar binary +*.zip binary + +## FONTS +*.ttf binary +*.eot binary +*.otf binary +*.woff binary +*.woff2 binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3de41fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,153 @@ +###################### +# Project Specific +###################### +/src/main/webapp/content/css/main.css +/build/resources/main/static/** +/src/test/javascript/coverage/ + +###################### +# Node +###################### +/node/ +node_tmp/ +node_modules/ +npm-debug.log.* +/.awcache/* +/.cache-loader/* + +###################### +# SASS +###################### +.sass-cache/ + +###################### +# Eclipse +###################### +*.pydevproject +.project +.metadata +tmp/ +tmp/**/* +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath +.factorypath +/src/main/resources/rebel.xml + +# External tool builders +.externalToolBuilders/** + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + +# STS-specific +/.sts4-cache/* + +###################### +# IntelliJ +###################### +.idea/ +*.iml +*.iws +*.ipr +*.ids +*.orig +classes/ +out/ + +###################### +# Visual Studio Code +###################### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +###################### +# Maven +###################### +/log/ +/target/ + +###################### +# Gradle +###################### +.gradle/ +/build/ + +###################### +# Package Files +###################### +*.jar +*.war +*.ear +*.db + +###################### +# Windows +###################### +# Windows image file caches +Thumbs.db + +# Folder config file +Desktop.ini + +###################### +# Mac OSX +###################### +.DS_Store +.svn + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +###################### +# Directories +###################### +/bin/ +/deploy/ + +###################### +# Logs +###################### +*.log* + +###################### +# Others +###################### +*.class +*.*~ +*~ +.merge_file* + +###################### +# Gradle Wrapper +###################### +!gradle/wrapper/gradle-wrapper.jar + +###################### +# Maven Wrapper +###################### +!.mvn/wrapper/maven-wrapper.jar + +###################### +# ESLint +###################### +.eslintcache diff --git a/.huskyrc b/.huskyrc new file mode 100644 index 0000000..4d077c8 --- /dev/null +++ b/.huskyrc @@ -0,0 +1,5 @@ +{ + "hooks": { + "pre-commit": "lint-staged" + } +} diff --git a/.jhipster/CustomerDetails.json b/.jhipster/CustomerDetails.json new file mode 100644 index 0000000..b420a9f --- /dev/null +++ b/.jhipster/CustomerDetails.json @@ -0,0 +1,62 @@ +{ + "name": "CustomerDetails", + "fields": [ + { + "fieldName": "gender", + "fieldType": "Gender", + "fieldValues": "MALE,FEMALE,OTHER", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "phone", + "fieldType": "String", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "addressLine1", + "fieldType": "String", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "addressLine2", + "fieldType": "String" + }, + { + "fieldName": "city", + "fieldType": "String", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "country", + "fieldType": "String", + "fieldValidateRules": ["required"] + } + ], + "relationships": [ + { + "relationshipType": "one-to-one", + "otherEntityName": "user", + "otherEntityRelationshipName": "customerDetails", + "relationshipValidateRules": "required", + "relationshipName": "user", + "otherEntityField": "login", + "ownerSide": true + }, + { + "relationshipType": "one-to-many", + "otherEntityName": "shoppingCart", + "otherEntityRelationshipName": "customerDetails", + "relationshipName": "cart" + } + ], + "changelogDate": "20200424080300", + "entityTableName": "customer_details", + "dto": "no", + "pagination": "pagination", + "service": "serviceClass", + "jpaMetamodelFiltering": false, + "fluentMethods": true, + "readOnly": false, + "embedded": false, + "applications": ["store"] +} diff --git a/.jhipster/Product.json b/.jhipster/Product.json new file mode 100644 index 0000000..8db6b85 --- /dev/null +++ b/.jhipster/Product.json @@ -0,0 +1,52 @@ +{ + "name": "Product", + "fields": [ + { + "fieldName": "name", + "fieldType": "String", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "description", + "fieldType": "String" + }, + { + "fieldName": "price", + "fieldType": "BigDecimal", + "fieldValidateRules": ["required", "min"], + "fieldValidateRulesMin": "0" + }, + { + "fieldName": "itemSize", + "fieldType": "Size", + "fieldValues": "S,M,L,XL,XXL", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "image", + "fieldType": "byte[]", + "fieldTypeBlobContent": "image" + } + ], + "relationships": [ + { + "relationshipType": "many-to-one", + "otherEntityName": "productCategory", + "otherEntityRelationshipName": "product", + "relationshipValidateRules": "required", + "relationshipName": "productCategory", + "otherEntityField": "name" + } + ], + "changelogDate": "20200424080100", + "javadoc": "Product sold by the Online store", + "entityTableName": "product", + "dto": "no", + "pagination": "pagination", + "service": "serviceClass", + "jpaMetamodelFiltering": false, + "fluentMethods": true, + "readOnly": false, + "embedded": false, + "applications": ["store"] +} diff --git a/.jhipster/ProductCategory.json b/.jhipster/ProductCategory.json new file mode 100644 index 0000000..074a593 --- /dev/null +++ b/.jhipster/ProductCategory.json @@ -0,0 +1,32 @@ +{ + "name": "ProductCategory", + "fields": [ + { + "fieldName": "name", + "fieldType": "String", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "description", + "fieldType": "String" + } + ], + "relationships": [ + { + "relationshipType": "one-to-many", + "otherEntityName": "product", + "otherEntityRelationshipName": "productCategory", + "relationshipName": "product" + } + ], + "changelogDate": "20200424080200", + "entityTableName": "product_category", + "dto": "no", + "pagination": "pagination", + "service": "serviceClass", + "jpaMetamodelFiltering": false, + "fluentMethods": true, + "readOnly": false, + "embedded": false, + "applications": ["store"] +} diff --git a/.jhipster/ProductOrder.json b/.jhipster/ProductOrder.json new file mode 100644 index 0000000..5ab3f18 --- /dev/null +++ b/.jhipster/ProductOrder.json @@ -0,0 +1,45 @@ +{ + "name": "ProductOrder", + "fields": [ + { + "fieldName": "quantity", + "fieldType": "Integer", + "fieldValidateRules": ["required", "min"], + "fieldValidateRulesMin": "0" + }, + { + "fieldName": "totalPrice", + "fieldType": "BigDecimal", + "fieldValidateRules": ["required", "min"], + "fieldValidateRulesMin": "0" + } + ], + "relationships": [ + { + "relationshipType": "many-to-one", + "otherEntityName": "product", + "otherEntityRelationshipName": "productOrder", + "relationshipValidateRules": "required", + "relationshipName": "product", + "otherEntityField": "name" + }, + { + "relationshipType": "many-to-one", + "otherEntityName": "shoppingCart", + "otherEntityRelationshipName": "order", + "relationshipValidateRules": "required", + "relationshipName": "cart", + "otherEntityField": "id" + } + ], + "changelogDate": "20200424080500", + "entityTableName": "product_order", + "dto": "no", + "pagination": "no", + "service": "serviceClass", + "jpaMetamodelFiltering": false, + "fluentMethods": true, + "readOnly": false, + "embedded": false, + "applications": ["store"] +} diff --git a/.jhipster/ShoppingCart.json b/.jhipster/ShoppingCart.json new file mode 100644 index 0000000..3b0028d --- /dev/null +++ b/.jhipster/ShoppingCart.json @@ -0,0 +1,62 @@ +{ + "name": "ShoppingCart", + "fields": [ + { + "fieldName": "placedDate", + "fieldType": "Instant", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "status", + "fieldType": "OrderStatus", + "fieldValues": "REFUND_INITIATED, REFUND_FAILED, PAID, PENDING, OPEN, CANCELLED, REFUNDED", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "totalPrice", + "fieldType": "BigDecimal", + "fieldValidateRules": ["required", "min"], + "fieldValidateRulesMin": "0" + }, + { + "fieldName": "paymentMethod", + "fieldType": "PaymentMethod", + "fieldValues": "CREDIT_CARD (scheme),IDEAL (ideal)", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "paymentReference", + "fieldType": "String" + }, + { + "fieldName": "paymentModificationReference", + "fieldType": "String" + } + ], + "relationships": [ + { + "relationshipType": "one-to-many", + "otherEntityName": "productOrder", + "otherEntityRelationshipName": "cart", + "relationshipName": "order" + }, + { + "relationshipType": "many-to-one", + "otherEntityName": "customerDetails", + "otherEntityRelationshipName": "cart", + "relationshipValidateRules": "required", + "relationshipName": "customerDetails", + "otherEntityField": "id" + } + ], + "changelogDate": "20200424080400", + "entityTableName": "shopping_cart", + "dto": "no", + "pagination": "no", + "service": "serviceClass", + "jpaMetamodelFiltering": false, + "fluentMethods": true, + "readOnly": false, + "embedded": false, + "applications": ["store"] +} diff --git a/.lintstagedrc.js b/.lintstagedrc.js new file mode 100644 index 0000000..dfb4f84 --- /dev/null +++ b/.lintstagedrc.js @@ -0,0 +1,3 @@ +module.exports = { + '{,src/**/,webpack/}*.{md,json,yml,html,js,ts,tsx,css,scss,java}': ['prettier --write'], +}; diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..80bcbed --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps = true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..ab0567f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules +target +build +package-lock.json +.git +.mvn +gradle +.gradle diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..f9b9911 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,18 @@ +# Prettier configuration + +printWidth: 140 +singleQuote: true +tabWidth: 2 +useTabs: false + +# js and ts rules: +arrowParens: avoid + +# jsx and tsx rules: +jsxBracketSameLine: false + +# java rules: +overrides: + - files: "*.java" + options: + tabWidth: 4 diff --git a/.yo-rc.json b/.yo-rc.json new file mode 100644 index 0000000..bc1be99 --- /dev/null +++ b/.yo-rc.json @@ -0,0 +1,50 @@ +{ + "generator-jhipster": { + "authenticationType": "jwt", + "cacheProvider": "ehcache", + "clientFramework": "react", + "serverPort": "8080", + "serviceDiscoveryType": false, + "skipUserManagement": false, + "withAdminUi": true, + "baseName": "store", + "buildTool": "gradle", + "databaseType": "sql", + "devDatabaseType": "h2Disk", + "enableHibernateCache": true, + "enableSwaggerCodegen": false, + "enableTranslation": false, + "jhiPrefix": "jhi", + "languages": [], + "messageBroker": false, + "prodDatabaseType": "mysql", + "searchEngine": false, + "skipClient": false, + "testFrameworks": [], + "websocket": false, + "packageName": "com.adyen.demo.store", + "packageFolder": "com/adyen/demo/store", + "jhipsterVersion": "7.0.0", + "skipServer": false, + "clientPackageManager": "npm", + "dtoSuffix": "DTO", + "entitySuffix": "", + "reactive": false, + "clientTheme": "none", + "clientThemeVariant": "", + "applicationType": "monolith", + "entities": ["Product", "ProductCategory", "CustomerDetails", "ShoppingCart", "ProductOrder"], + "skipCheckLengthOfIdentifier": false, + "skipFakeData": false, + "blueprints": [], + "otherModules": [], + "pages": [], + "nativeLanguage": "en", + "creationTimestamp": 1608565320758, + "jwtSecretKey": "NTMyMGZkYWMzYTVmOWMwMWUwOWNhYjllYzM0MTg2ZDZmNzZmMWZhNjcwYjc2ODA2ZGZjMGJlYTc4YzM3YzczMTFiNjBlYjczMWM4NDM3NTM0N2RkOTNiMWJmOWE4YTUxOTkzMzQ0Zjc2MTNmN2U4NjMyZGMzYjRiNzUwZTI2OTA=", + "lastLiquibaseTimestamp": 1616596655000, + "herokuAppName": "deepu-ecom-test-888", + "herokuDeployType": "jar", + "herokuJavaVersion": "11" + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..57a3ab0 --- /dev/null +++ b/README.md @@ -0,0 +1,189 @@ +# store + +This application was generated using JHipster 7.0.0, you can find documentation and help at [https://www.jhipster.tech/documentation-archive/v7.0.0](https://www.jhipster.tech/documentation-archive/v7.0.0). + +## Development + +Before you can build this project, you must install and configure the following dependencies on your machine: + +1. [Node.js][]: We use Node to run a development web server and build the project. + Depending on your system, you can install Node either from source or as a pre-packaged bundle. + +After installing Node, you should be able to run the following command to install development tools. +You will only need to run this command when dependencies change in [package.json](package.json). + +``` +npm install +``` + +We use npm scripts and [Webpack][] as our build system. + +Run the following commands in two separate terminals to create a blissful development experience where your browser +auto-refreshes when files change on your hard drive. + +``` +./gradlew -x webapp +npm start +``` + +Npm is also used to manage CSS and JavaScript dependencies used in this application. You can upgrade dependencies by +specifying a newer version in [package.json](package.json). You can also run `npm update` and `npm install` to manage dependencies. +Add the `help` flag on any command to see how you can use it. For example, `npm help update`. + +The `npm run` command will list all of the scripts available to run for this project. + +### PWA Support + +JHipster ships with PWA (Progressive Web App) support, and it's turned off by default. One of the main components of a PWA is a service worker. + +The service worker initialization code is commented out by default. To enable it, uncomment the following code in `src/main/webapp/index.html`: + +```html + +``` + +Note: [Workbox](https://developers.google.com/web/tools/workbox/) powers JHipster's service worker. It dynamically generates the `service-worker.js` file. + +### Managing dependencies + +For example, to add [Leaflet][] library as a runtime dependency of your application, you would run following command: + +``` +npm install --save --save-exact leaflet +``` + +To benefit from TypeScript type definitions from [DefinitelyTyped][] repository in development, you would run following command: + +``` +npm install --save-dev --save-exact @types/leaflet +``` + +Then you would import the JS and CSS files specified in library's installation instructions so that [Webpack][] knows about them: +Note: There are still a few other things remaining to do for Leaflet that we won't detail here. + +For further instructions on how to develop with JHipster, have a look at [Using JHipster in development][]. + +## Building for production + +### Packaging as jar + +To build the final jar and optimize the store application for production, run: + +``` +./gradlew -Pprod clean bootJar +``` + +This will concatenate and minify the client CSS and JavaScript files. It will also modify `index.html` so it references these new files. +To ensure everything worked, run: + +``` +java -jar build/libs/*.jar +``` + +Then navigate to [http://localhost:8080](http://localhost:8080) in your browser. + +Refer to [Using JHipster in production][] for more details. + +### Packaging as war + +To package your application as a war in order to deploy it to an application server, run: + +``` +./gradlew -Pprod -Pwar clean bootWar +``` + +## Testing + +To launch your application's tests, run: + +``` +./gradlew test integrationTest jacocoTestReport +``` + +### Client tests + +Unit tests are run by [Jest][]. They're located in [src/test/javascript/](src/test/javascript/) and can be run with: + +``` +npm test +``` + +For more information, refer to the [Running tests page][]. + +### Code quality + +Sonar is used to analyse code quality. You can start a local Sonar server (accessible on http://localhost:9001) with: + +``` +docker-compose -f src/main/docker/sonar.yml up -d +``` + +Note: we have turned off authentication in [src/main/docker/sonar.yml](src/main/docker/sonar.yml) for out of the box experience while trying out SonarQube, for real use cases turn it back on. + +You can run a Sonar analysis with using the [sonar-scanner](https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner) or by using the gradle plugin. + +Then, run a Sonar analysis: + +``` +./gradlew -Pprod clean check jacocoTestReport sonarqube +``` + +For more information, refer to the [Code quality page][]. + +## Using Docker to simplify development (optional) + +You can use Docker to improve your JHipster development experience. A number of docker-compose configuration are available in the [src/main/docker](src/main/docker) folder to launch required third party services. + +For example, to start a mysql database in a docker container, run: + +``` +docker-compose -f src/main/docker/mysql.yml up -d +``` + +To stop it and remove the container, run: + +``` +docker-compose -f src/main/docker/mysql.yml down +``` + +You can also fully dockerize your application and all the services that it depends on. +To achieve this, first build a docker image of your app by running: + +``` +./gradlew bootJar -Pprod jibDockerBuild +``` + +Then run: + +``` +docker-compose -f src/main/docker/app.yml up -d +``` + +For more information refer to [Using Docker and Docker-Compose][], this page also contains information on the docker-compose sub-generator (`jhipster docker-compose`), which is able to generate docker configurations for one or several JHipster applications. + +## Continuous Integration (optional) + +To configure CI for your project, run the ci-cd sub-generator (`jhipster ci-cd`), this will let you generate configuration files for a number of Continuous Integration systems. Consult the [Setting up Continuous Integration][] page for more information. + +[jhipster homepage and latest documentation]: https://www.jhipster.tech +[jhipster 7.0.0 archive]: https://www.jhipster.tech/documentation-archive/v7.0.0 +[using jhipster in development]: https://www.jhipster.tech/documentation-archive/v7.0.0/development/ +[using docker and docker-compose]: https://www.jhipster.tech/documentation-archive/v7.0.0/docker-compose +[using jhipster in production]: https://www.jhipster.tech/documentation-archive/v7.0.0/production/ +[running tests page]: https://www.jhipster.tech/documentation-archive/v7.0.0/running-tests/ +[code quality page]: https://www.jhipster.tech/documentation-archive/v7.0.0/code-quality/ +[setting up continuous integration]: https://www.jhipster.tech/documentation-archive/v7.0.0/setting-up-ci/ +[node.js]: https://nodejs.org/ +[webpack]: https://webpack.github.io/ +[browsersync]: https://www.browsersync.io/ +[jest]: https://facebook.github.io/jest/ +[jasmine]: https://jasmine.github.io/2.0/introduction.html +[protractor]: https://angular.github.io/protractor/ +[leaflet]: https://leafletjs.com/ +[definitelytyped]: https://definitelytyped.org/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..360e59e --- /dev/null +++ b/build.gradle @@ -0,0 +1,301 @@ +buildscript { + repositories { + gradlePluginPortal() + } + dependencies { + //jhipster-needle-gradle-buildscript-dependency - JHipster will add additional gradle build script plugins here + } +} + +plugins { + id "java" + id "maven-publish" + id "idea" + id "eclipse" + id "jacoco" + id "org.springframework.boot" + id "com.google.cloud.tools.jib" + id "com.gorylenko.gradle-git-properties" + id "com.github.node-gradle.node" + id "org.liquibase.gradle" + id "org.sonarqube" + id "io.spring.nohttp" + //jhipster-needle-gradle-plugins - JHipster will add additional gradle plugins here +} + +group = "com.adyen.demo.store" +version = "0.0.1-SNAPSHOT" + +description = "" + +sourceCompatibility=11 +targetCompatibility=11 +assert System.properties["java.specification.version"] == "1.8" || "1.9" || "10" || "11" || "12" || "13" || "14" || "15" + +apply from: "gradle/docker.gradle" +apply from: "gradle/sonar.gradle" +//jhipster-needle-gradle-apply-from - JHipster will add additional gradle scripts to be applied here + +if (project.hasProperty("prod") || project.hasProperty("gae")) { + apply from: "gradle/profile_prod.gradle" +} else { + apply from: "gradle/profile_dev.gradle" +} + +if (project.hasProperty("war")) { + apply from: "gradle/war.gradle" +} + +if (project.hasProperty("gae")) { + apply plugin: 'maven' + apply plugin: 'org.springframework.boot.experimental.thin-launcher' + apply plugin: 'io.spring.dependency-management' + + dependencyManagement { + imports { + mavenBom "tech.jhipster:jhipster-dependencies:${jhipsterDependenciesVersion}" + } + } + appengineStage.dependsOn thinResolve +} + + +idea { + module { + excludeDirs += files("node_modules") + } +} + +eclipse { + sourceSets { + main { + java { + srcDirs += ["build/generated/sources/annotationProcessor/java/main"] + } + } + } +} + +defaultTasks "bootRun" + +springBoot { + mainClassName = "com.adyen.demo.store.StoreApp" +} + +test { + useJUnitPlatform() + exclude "**/*IT*", "**/*IntTest*" + + testLogging { + events 'FAILED', 'SKIPPED' + } + // uncomment if the tests reports are not generated + // see https://github.com/jhipster/generator-jhipster/pull/2771 and https://github.com/jhipster/generator-jhipster/pull/4484 + // ignoreFailures true + reports.html.enabled = false +} + +task integrationTest(type: Test) { + useJUnitPlatform() + description = "Execute integration tests." + group = "verification" + include "**/*IT*", "**/*IntTest*" + + testLogging { + events 'FAILED', 'SKIPPED' + } + + if (project.hasProperty('testcontainers')) { + environment 'spring.profiles.active', 'testcontainers' + } + + // uncomment if the tests reports are not generated + // see https://github.com/jhipster/generator-jhipster/pull/2771 and https://github.com/jhipster/generator-jhipster/pull/4484 + // ignoreFailures true + reports.html.enabled = false +} + +check.dependsOn integrationTest +task testReport(type: TestReport) { + destinationDir = file("$buildDir/reports/tests") + reportOn test +} + +task integrationTestReport(type: TestReport) { + destinationDir = file("$buildDir/reports/tests") + reportOn integrationTest +} + +if (!project.hasProperty("runList")) { + project.ext.runList = "main" +} + +project.ext.diffChangelogFile = "src/main/resources/config/liquibase/changelog/" + new Date().format("yyyyMMddHHmmss") + "_changelog.xml" + +liquibase { + activities { + main { + driver "org.h2.Driver" + url "jdbc:h2:file:./build/h2db/db/store" + username "store" + password "" + changeLogFile "src/main/resources/config/liquibase/master.xml" + defaultSchemaName "" + logLevel "debug" + classpath "src/main/resources/" + } + diffLog { + driver "org.h2.Driver" + url "jdbc:h2:file:./build/h2db/db/store" + username "store" + password "" + changeLogFile project.ext.diffChangelogFile + referenceUrl "hibernate:spring:com.adyen.demo.store.domain?dialect=org.hibernate.dialect.H2Dialect&hibernate.physical_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy" + defaultSchemaName "" + logLevel "debug" + classpath "$buildDir/classes/java/main" + } + } + + runList = project.ext.runList +} + +gitProperties { + failOnNoGitDirectory = false + keys = ["git.branch", "git.commit.id.abbrev", "git.commit.id.describe"] +} + +checkstyle { + toolVersion "${checkstyleVersion}" + configFile file("checkstyle.xml") + checkstyleTest.enabled = false +} +nohttp { + source.include "build.gradle", "README.md" +} + +configurations { + providedRuntime + implementation.exclude module: "spring-boot-starter-tomcat" + all { + resolutionStrategy { + // Inherited version from Spring Boot can't be used because of regressions: + // To be removed as soon as spring-boot use the same version + force 'org.liquibase:liquibase-core:4.3.1' + } + } +} + +repositories { + mavenLocal() + mavenCentral() + //jhipster-needle-gradle-repositories - JHipster will add additional repositories +} + +dependencies { + // import JHipster dependencies BOM + if (!project.hasProperty("gae")) { + implementation platform("tech.jhipster:jhipster-dependencies:${jhipsterDependenciesVersion}") + } + + // Use ", version: jhipsterDependenciesVersion, changing: true" if you want + // to use a SNAPSHOT release instead of a stable release + implementation group: "tech.jhipster", name: "jhipster-framework" + implementation "javax.annotation:javax.annotation-api" + implementation "org.springframework.boot:spring-boot-starter-cache" + implementation "io.dropwizard.metrics:metrics-core" + implementation "io.micrometer:micrometer-registry-prometheus" + implementation "com.fasterxml.jackson.datatype:jackson-datatype-hppc" + implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" + implementation "com.fasterxml.jackson.module:jackson-module-jaxb-annotations" + implementation "com.fasterxml.jackson.datatype:jackson-datatype-hibernate5" + implementation "com.fasterxml.jackson.core:jackson-annotations" + implementation "com.fasterxml.jackson.core:jackson-databind" + implementation "javax.cache:cache-api" + implementation "org.hibernate:hibernate-core" + implementation "com.zaxxer:HikariCP" + implementation "org.apache.commons:commons-lang3" + implementation "javax.transaction:javax.transaction-api" + implementation "org.ehcache:ehcache" + implementation "org.hibernate:hibernate-jcache" + implementation "org.hibernate:hibernate-entitymanager" + implementation "org.hibernate.validator:hibernate-validator" + implementation "org.liquibase:liquibase-core" + liquibaseRuntime "org.liquibase:liquibase-core" + liquibaseRuntime "org.liquibase.ext:liquibase-hibernate5:${liquibaseHibernate5Version}" + liquibaseRuntime sourceSets.main.compileClasspath + implementation "org.springframework.boot:spring-boot-loader-tools" + implementation "org.springframework.boot:spring-boot-starter-mail" + implementation "org.springframework.boot:spring-boot-starter-logging" + implementation "org.springframework.boot:spring-boot-starter-actuator" + implementation "org.springframework.boot:spring-boot-starter-data-jpa" + testImplementation "org.testcontainers:mysql" + implementation "org.springframework.boot:spring-boot-starter-security" + implementation ("org.springframework.boot:spring-boot-starter-web") { + exclude module: "spring-boot-starter-tomcat" + } + implementation "org.springframework.boot:spring-boot-starter-undertow" + implementation "org.springframework.boot:spring-boot-starter-thymeleaf" + implementation "org.zalando:problem-spring-web" + implementation "org.springframework.security:spring-security-config" + + implementation "org.springframework.security:spring-security-data" + implementation "org.springframework.security:spring-security-web" + implementation "io.jsonwebtoken:jjwt-api" + if (!project.hasProperty("gae")) { + runtimeOnly "io.jsonwebtoken:jjwt-impl" + runtimeOnly "io.jsonwebtoken:jjwt-jackson" + } else { + implementation "io.jsonwebtoken:jjwt-impl" + implementation "io.jsonwebtoken:jjwt-jackson" + } + implementation ("io.springfox:springfox-oas") + implementation ("io.springfox:springfox-swagger2") + implementation "io.springfox:springfox-bean-validators" + implementation "mysql:mysql-connector-java" + liquibaseRuntime "mysql:mysql-connector-java" + implementation "org.mapstruct:mapstruct:${mapstructVersion}" + annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}" + annotationProcessor "org.hibernate:hibernate-jpamodelgen:${hibernateVersion}" + annotationProcessor "org.glassfish.jaxb:jaxb-runtime:${jaxbRuntimeVersion}" + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}" + testImplementation "org.springframework.boot:spring-boot-starter-test" + testImplementation "org.springframework.security:spring-security-test" + testImplementation "org.springframework.boot:spring-boot-test" + testImplementation "com.tngtech.archunit:archunit-junit5-api:${archunitJunit5Version}" + testRuntimeOnly "com.tngtech.archunit:archunit-junit5-engine:${archunitJunit5Version}" + testImplementation "com.h2database:h2" + liquibaseRuntime "com.h2database:h2" + //jhipster-needle-gradle-dependency - JHipster will add additional dependencies here +} + +if (project.hasProperty("gae")) { + task createPom { + def basePath = 'build/resources/main/META-INF/maven' + doLast { + pom { + withXml(dependencyManagement.pomConfigurer) + }.writeTo("${basePath}/${project.group}/${project.name}/pom.xml") + } + } + bootJar.dependsOn = [createPom] +} + +task cleanResources(type: Delete) { + delete "build/resources" +} + +wrapper { + gradleVersion = "6.8.3" +} + + +if (project.hasProperty("nodeInstall")) { + node { + version = "14.16.0" + npmVersion = "7.6.3" + download = true + } +} +compileJava.dependsOn processResources +processResources.dependsOn bootBuildInfo diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..4c6a041 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f292fe8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,59 @@ +rootProject.name=store +profile=dev + +# Dependency versions +jhipsterDependenciesVersion=7.0.0 +# The spring-boot version should match the one managed by +# https://mvnrepository.com/artifact/tech.jhipster/jhipster-dependencies/7.0.0 +springBootVersion=2.4.4 +# The hibernate version should match the one managed by +# https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-dependencies/2.4.4 --> +hibernateVersion=5.4.29.Final +mapstructVersion=1.4.2.Final +archunitJunit5Version=0.17.0 +liquibaseHibernate5Version=4.3.1 +liquibaseTaskPrefix=liquibase + + +jaxbRuntimeVersion=2.3.3 + +# gradle plugin version +jibPluginVersion=2.8.0 +gitPropertiesPluginVersion=2.2.4 +gradleNodePluginVersion=3.0.1 +liquibasePluginVersion=2.0.4 +sonarqubePluginVersion=3.1.1 +springNoHttpPluginVersion=0.0.5.RELEASE +checkstyleVersion=8.40 + +# jhipster-needle-gradle-property - JHipster will add additional properties here + +## below are some of the gradle performance improvement settings that can be used as required, these are not enabled by default + +## The Gradle daemon aims to improve the startup and execution time of Gradle. +## The daemon is enabled by default in Gradle 3+ setting this to false will disable this. +## https://docs.gradle.org/current/userguide/gradle_daemon.html#sec:ways_to_disable_gradle_daemon +## uncomment the below line to disable the daemon + +#org.gradle.daemon=false + +## Specifies the JVM arguments used for the daemon process. +## The setting is particularly useful for tweaking memory settings. +## Default value: -Xmx1024m -XX:MaxPermSize=256m +## uncomment the below line to override the daemon defaults + +#org.gradle.jvmargs=-Xmx1024m -XX:MaxPermSize=256m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +## When configured, Gradle will run in incubating parallel mode. +## This option should only be used with decoupled projects. More details, visit +## http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +## uncomment the below line to enable parallel mode + +#org.gradle.parallel=true + +## Enables new incubating mode that makes Gradle selective when configuring projects. +## Only relevant projects are configured which results in faster builds for large multi-projects. +## http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:configuration_on_demand +## uncomment the below line to enable the selective mode + +#org.gradle.configureondemand=true diff --git a/gradle/docker.gradle b/gradle/docker.gradle new file mode 100644 index 0000000..ad85634 --- /dev/null +++ b/gradle/docker.gradle @@ -0,0 +1,23 @@ +jib { + from { + image = "adoptopenjdk:11-jre-hotspot" + } + to { + image = "store:latest" + } + container { + entrypoint = ["bash", "-c", "/entrypoint.sh"] + ports = ["8080"] + environment = [ + SPRING_OUTPUT_ANSI_ENABLED: "ALWAYS", + JHIPSTER_SLEEP: "0" + ] + creationTime = "USE_CURRENT_TIMESTAMP" + user = 1000 + } + extraDirectories { + paths = file("src/main/docker/jib") + permissions = ["/entrypoint.sh": "755"] + } +} + diff --git a/gradle/profile_dev.gradle b/gradle/profile_dev.gradle new file mode 100644 index 0000000..cf78840 --- /dev/null +++ b/gradle/profile_dev.gradle @@ -0,0 +1,56 @@ +dependencies { + developmentOnly "org.springframework.boot:spring-boot-devtools:${springBootVersion}" + implementation "com.h2database:h2" +} + +def profiles = "dev" +if (project.hasProperty("no-liquibase")) { + profiles += ",no-liquibase" +} +if (project.hasProperty("tls")) { + profiles += ",tls" +} + +springBoot { + buildInfo { + properties { + time = null + } + } +} + +bootRun { + args = [] +} + +task webapp(type: NpmTask) { + inputs.files("package-lock.json") + inputs.files("build.gradle") + inputs.dir("src/main/webapp/") + + def webpackDevFiles = fileTree("webpack/") + webpackDevFiles.exclude("webpack.prod.js") + inputs.files(webpackDevFiles) + + outputs.dir("build/resources/main/static/") + + dependsOn npmInstall + args = ["run", "webapp:build"] + environment = [APP_VERSION: project.version] +} + +processResources { + inputs.property('version', version) + inputs.property('springProfiles', profiles) + filesMatching("**/application.yml") { + filter { + it.replace("#project.version#", version) + } + filter { + it.replace("#spring.profiles.active#", profiles) + } + } +} + +processResources.dependsOn webapp +bootJar.dependsOn processResources diff --git a/gradle/profile_prod.gradle b/gradle/profile_prod.gradle new file mode 100644 index 0000000..e9e381e --- /dev/null +++ b/gradle/profile_prod.gradle @@ -0,0 +1,46 @@ +dependencies { + testImplementation "com.h2database:h2" +} + +def profiles = "prod" +if (project.hasProperty("no-liquibase")) { + profiles += ",no-liquibase" +} + +if (project.hasProperty("api-docs")) { + profiles += ",api-docs" +} + +springBoot { + buildInfo() +} + +bootRun { + args = [] +} + +task webapp_test(type: NpmTask, dependsOn: "npmInstall") { + args = ["run", "webapp:test"] +} + +task webapp(type: NpmTask, dependsOn: "npmInstall") { + args = ["run", "webapp:prod"] + environment = [APP_VERSION: project.version] +} + +processResources { + inputs.property('version', version) + inputs.property('springProfiles', profiles) + filesMatching("**/application.yml") { + filter { + it.replace("#project.version#", version) + } + filter { + it.replace("#spring.profiles.active#", profiles) + } + } +} + +test.dependsOn webapp_test +processResources.dependsOn webapp +bootJar.dependsOn processResources diff --git a/gradle/sonar.gradle b/gradle/sonar.gradle new file mode 100644 index 0000000..e65a6ad --- /dev/null +++ b/gradle/sonar.gradle @@ -0,0 +1,26 @@ +jacoco { + toolVersion = "0.8.6" +} + +jacocoTestReport { + executionData tasks.withType(Test) + classDirectories.from = files(sourceSets.main.output.classesDirs) + sourceDirectories.from = files(sourceSets.main.java.srcDirs) + + reports { + xml.enabled = true + } +} + +file("sonar-project.properties").withReader { + Properties sonarProperties = new Properties() + sonarProperties.load(it) + + sonarProperties.each { key, value -> + sonarqube { + properties { + property key, value + } + } + } +} diff --git a/gradle/war.gradle b/gradle/war.gradle new file mode 100644 index 0000000..2e8a621 --- /dev/null +++ b/gradle/war.gradle @@ -0,0 +1,16 @@ +apply plugin: "war" + +bootWar { + mainClassName = "com.adyen.demo.store.StoreApp" + includes = ["WEB-INF/**", "META-INF/**"] + webXml = file("${project.rootDir}/src/main/webapp/WEB-INF/web.xml") +} + +war { + webAppDirName = "build/resources/main/static/" + webXml = file("${project.rootDir}/src/main/webapp/WEB-INF/web.xml") + enabled = true + archiveExtension = "war.original" + includes = ["WEB-INF/**", "META-INF/**"] + +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..5c2d1cf016b3885f6930543d57b744ea8c220a1a GIT binary patch literal 55616 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/jest.conf.js b/jest.conf.js new file mode 100644 index 0000000..d9a90a2 --- /dev/null +++ b/jest.conf.js @@ -0,0 +1,56 @@ +const tsconfig = require('./tsconfig.json'); + +module.exports = { + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + testURL: 'http://localhost/', + cacheDirectory: '/build/jest-cache', + coverageDirectory: '/build/test-results/', + testMatch: ['/src/main/webapp/app/**/@(*.)@(spec.ts?(x))'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + coveragePathIgnorePatterns: ['/src/test/javascript/'], + moduleNameMapper: mapTypescriptAliasToJestAlias({ + '\\.(css|scss)$': 'identity-obj-proxy', + }), + reporters: ['default', ['jest-junit', { outputDirectory: './build/test-results/', outputName: 'TESTS-results-jest.xml' }]], + testResultsProcessor: 'jest-sonar-reporter', + testPathIgnorePatterns: ['/node_modules/'], + setupFiles: ['/src/main/webapp/app/setup-tests.ts'], + globals: { + 'ts-jest': { + tsconfig: './tsconfig.test.json', + compiler: 'typescript', + diagnostics: false, + }, + }, +}; + +function mapTypescriptAliasToJestAlias(alias = {}) { + const jestAliases = { ...alias }; + if (!tsconfig.compilerOptions.paths) { + return jestAliases; + } + Object.entries(tsconfig.compilerOptions.paths) + .filter(([key, value]) => { + // use Typescript alias in Jest only if this has value + if (value.length) { + return true; + } + return false; + }) + .map(([key, value]) => { + // if Typescript alias ends with /* then in Jest: + // - alias key must end with /(.*) + // - alias value must end with /$1 + const regexToReplace = /(.*)\/\*$/; + const aliasKey = key.replace(regexToReplace, '$1/(.*)'); + const aliasValue = value[0].replace(regexToReplace, '$1/$$1'); + return [aliasKey, `/${aliasValue}`]; + }) + .reduce((aliases, [key, value]) => { + aliases[key] = value; + return aliases; + }, jestAliases); + return jestAliases; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7ab7f55 --- /dev/null +++ b/package.json @@ -0,0 +1,178 @@ +{ + "name": "store", + "version": "0.0.1-SNAPSHOT", + "private": true, + "description": "Description for store", + "license": "UNLICENSED", + "scripts": { + "prettier:check": "prettier --check \"{,src/**/,webpack/}*.{md,json,yml,html,js,ts,tsx,css,scss,java}\"", + "prettier:format": "prettier --write \"{,src/**/,webpack/}*.{md,json,yml,html,js,ts,tsx,css,scss,java}\"", + "lint": "eslint . --ext .js,.ts,.jsx,.tsx", + "lint:fix": "npm run lint -- --fix", + "cleanup": "rimraf build/resources/main/static/", + "clean-www": "rimraf build/resources/main/static/app/{src,build/}", + "jest": "jest --coverage --logHeapUsage --maxWorkers=2 --config jest.conf.js", + "jest:update": "npm run jest -- --updateSnapshot", + "start": "npm run webapp:dev", + "start-tls": "npm run webapp:dev -- --env.tls", + "pretest": "npm run lint", + "test": "npm run jest", + "test-ci": "npm run lint && npm run jest:update", + "test:watch": "npm run jest -- --watch", + "webapp:build": "npm run clean-www && npm run webapp:build:dev", + "webapp:build:dev": "npm run webpack -- --config webpack/webpack.dev.js --env stats=minimal", + "webapp:build:prod": "npm run webpack -- --config webpack/webpack.prod.js --progress=profile", + "webapp:dev": "npm run webpack-dev-server -- --config webpack/webpack.dev.js --inline --port=9060 --env stats=minimal", + "webapp:dev-verbose": "npm run webpack-dev-server -- --config webpack/webpack.dev.js --inline --port=9060 --progress=profile --env stats=normal", + "webapp:prod": "npm run clean-www && npm run webapp:build:prod", + "webapp:test": "npm run test", + "webpack-dev-server": "npm run webpack -- serve", + "webpack": "node --max_old_space_size=4096 node_modules/webpack/bin/webpack.js", + "docker:db:up": "docker-compose -f src/main/docker/mysql.yml up -d", + "docker:db:down": "docker-compose -f src/main/docker/mysql.yml down -v --remove-orphans", + "docker:others:await": "", + "predocker:others:up": "", + "docker:others:up": "", + "docker:others:down": "", + "ci:e2e:prepare:docker": "npm run docker:db:up && npm run docker:others:up && docker ps -a", + "ci:e2e:prepare": "npm run ci:e2e:prepare:docker", + "ci:e2e:teardown:docker": "npm run docker:db:down --if-present && npm run docker:others:down && docker ps -a", + "ci:e2e:teardown": "npm run ci:e2e:teardown:docker", + "backend:info": "./gradlew -v", + "backend:doc:test": "./gradlew javadoc -x webapp", + "backend:nohttp:test": "./gradlew checkstyleNohttp -x webapp", + "java:jar": "./gradlew bootJar -x test -x integrationTest", + "java:war": "./gradlew bootWar -Pwar -x test -x integrationTest", + "java:docker": "./gradlew bootJar jibDockerBuild", + "backend:unit:test": "./gradlew test integrationTest -x webapp -Dlogging.level.ROOT=OFF -Dlogging.level.org.zalando=OFF -Dlogging.level.tech.jhipster=OFF -Dlogging.level.com.adyen.demo.store=OFF -Dlogging.level.org.springframework=OFF -Dlogging.level.org.springframework.web=OFF -Dlogging.level.org.springframework.security=OFF", + "ci:e2e:package": "npm run java:$npm_package_config_packaging:$npm_package_config_default_environment -- -Pe2e -Denforcer.skip=true", + "postci:e2e:package": "cp build/libs/*SNAPSHOT.$npm_package_config_packaging e2e.$npm_package_config_packaging", + "java:jar:dev": "npm run java:jar -- -Pdev,webapp", + "java:jar:prod": "npm run java:jar -- -Pprod", + "java:war:dev": "npm run java:war -- -Pdev,webapp", + "java:war:prod": "npm run java:war -- -Pprod", + "java:docker:dev": "npm run java:docker -- -Pdev,webapp", + "java:docker:prod": "npm run java:docker -- -Pprod", + "ci:backend:test": "npm run backend:info && npm run backend:doc:test && npm run backend:nohttp:test && npm run backend:unit:test", + "ci:server:package": "npm run java:$npm_package_config_packaging:$npm_package_config_default_environment", + "preci:e2e:server:start": "npm run docker:db:await --if-present && npm run docker:others:await --if-present", + "ci:e2e:server:start": "java -jar e2e.$npm_package_config_packaging --spring.profiles.active=$npm_package_config_default_environment -Dlogging.level.ROOT=OFF -Dlogging.level.org.zalando=OFF -Dlogging.level.tech.jhipster=OFF -Dlogging.level.com.adyen.demo.store=OFF -Dlogging.level.org.springframework=OFF -Dlogging.level.org.springframework.web=OFF -Dlogging.level.org.springframework.security=OFF --logging.level.org.springframework.web=ERROR", + "ci:frontend:test": "npm run webapp:build:$npm_package_config_default_environment && npm run test-ci" + }, + "config": { + "backend_port": "8080", + "default_environment": "prod", + "packaging": "jar" + }, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "1.2.35", + "@fortawesome/free-solid-svg-icons": "5.15.3", + "@fortawesome/react-fontawesome": "0.1.14", + "availity-reactstrap-validation": "2.7.0", + "axios": "0.21.1", + "bootstrap": "4.6.0", + "dayjs": "1.10.4", + "loaders.css": "0.1.2", + "lodash": "4.17.21", + "path-browserify": "1.0.1", + "react": "17.0.1", + "react-dom": "17.0.1", + "react-hot-loader": "4.13.0", + "react-jhipster": "0.15.0", + "react-loadable": "5.5.0", + "react-redux": "7.2.2", + "react-redux-loading-bar": "5.0.0", + "react-router-dom": "5.2.0", + "react-toastify": "7.0.3", + "react-transition-group": "4.4.1", + "reactstrap": "8.9.0", + "redux": "4.0.5", + "redux-devtools": "3.7.0", + "redux-devtools-dock-monitor": "1.2.0", + "redux-devtools-log-monitor": "2.1.0", + "redux-promise-middleware": "6.1.2", + "redux-thunk": "2.3.0", + "tslib": "2.1.0", + "uuid": "8.3.2" + }, + "devDependencies": { + "@testing-library/react": "11.2.5", + "@types/jest": "26.0.20", + "@types/lodash": "4.14.168", + "@types/node": "14.14.35", + "@types/react": "17.0.3", + "@types/react-dom": "17.0.2", + "@types/react-redux": "7.1.16", + "@types/react-router-dom": "5.1.7", + "@types/redux": "3.6.31", + "@types/webpack-env": "1.16.0", + "@typescript-eslint/eslint-plugin": "4.18.0", + "@typescript-eslint/parser": "4.18.0", + "autoprefixer": "10.2.5", + "browser-sync": "2.26.14", + "browser-sync-webpack-plugin": "2.3.0", + "cache-loader": "4.1.0", + "concurrently": "6.0.0", + "copy-webpack-plugin": "8.0.0", + "core-js": "3.9.1", + "cross-env": "7.0.3", + "css-loader": "5.1.3", + "eslint": "7.22.0", + "eslint-config-prettier": "8.1.0", + "eslint-plugin-react": "7.22.0", + "eslint-webpack-plugin": "2.5.2", + "file-loader": "6.2.0", + "fork-ts-checker-webpack-plugin": "6.2.0", + "friendly-errors-webpack-plugin": "1.7.0", + "generator-jhipster": "7.0.0", + "html-webpack-plugin": "5.3.1", + "husky": "4.3.8", + "identity-obj-proxy": "3.0.0", + "jest": "26.6.3", + "jest-junit": "12.0.0", + "jest-sonar-reporter": "2.0.0", + "json-loader": "0.5.7", + "lint-staged": "10.5.4", + "mini-css-extract-plugin": "1.3.9", + "optimize-css-assets-webpack-plugin": "5.0.4", + "postcss-loader": "5.2.0", + "prettier": "2.2.1", + "prettier-plugin-java": "1.0.2", + "prettier-plugin-packagejson": "2.2.10", + "react-infinite-scroller": "1.2.4", + "redux-mock-store": "1.5.4", + "rimraf": "3.0.2", + "sass": "1.32.8", + "sass-loader": "11.0.1", + "simple-progress-webpack-plugin": "1.1.2", + "sinon": "9.2.4", + "source-map-loader": "2.0.1", + "sourcemap-istanbul-instrumenter-loader": "0.2.0", + "stripcomment-loader": "0.1.0", + "style-loader": "2.0.0", + "swagger-ui-dist": "3.45.0", + "terser-webpack-plugin": "5.1.1", + "thread-loader": "3.0.1", + "to-string-loader": "1.1.6", + "ts-jest": "26.5.3", + "ts-loader": "8.0.18", + "typescript": "4.2.3", + "wait-on": "5.2.1", + "webpack": "5.26.3", + "webpack-cli": "4.5.0", + "webpack-dev-server": "3.11.2", + "webpack-merge": "5.7.3", + "webpack-notifier": "1.13.0", + "workbox-webpack-plugin": "6.1.2" + }, + "engines": { + "node": ">=14.16.0" + }, + "cacheDirectories": [ + "node_modules" + ], + "jestSonar": { + "reportPath": "build/test-results/jest", + "reportFile": "TESTS-results-sonar.xml" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..a0fa32b --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,3 @@ +module.exports = { + plugins: [require('autoprefixer')], +}; diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..58b22f8 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,18 @@ + +pluginManagement { + repositories { + maven { url 'https://repo.spring.io/milestone' } + gradlePluginPortal() + } + plugins { + id 'org.springframework.boot' version "${springBootVersion}" + id 'com.google.cloud.tools.jib' version "${jibPluginVersion}" + id 'com.gorylenko.gradle-git-properties' version "${gitPropertiesPluginVersion}" + id 'com.github.node-gradle.node' version "${gradleNodePluginVersion}" + id 'org.liquibase.gradle' version "${liquibasePluginVersion}" + id 'org.sonarqube' version "${sonarqubePluginVersion}" + id "io.spring.nohttp" version "${springNoHttpPluginVersion}" + } +} + +rootProject.name = "store" diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..815fdf6 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,30 @@ +sonar.projectKey=store +sonar.projectName=store generated by jhipster +sonar.projectVersion=1.0 + +sonar.sources=src/main/ +sonar.host.url=http://localhost:9001 + +sonar.test.inclusions=src/test/**/*.*, src/main/webapp/app/**/*.spec.ts, src/main/webapp/app/**/*.spec.tsx +sonar.coverage.jacoco.xmlReportPaths=build/reports/jacoco/test/jacocoTestReport.xml +sonar.java.codeCoveragePlugin=jacoco +sonar.junit.reportPaths=build/test-results/test, build/test-results/integrationTest +sonar.testExecutionReportPaths=build/test-results/jest/TESTS-results-sonar.xml +sonar.javascript.lcov.reportPaths=build/test-results/lcov.info + +sonar.sourceEncoding=UTF-8 +sonar.exclusions=src/main/webapp/content/**/*.*, src/main/webapp/i18n/*.js, build/resources/main/static/**/*.* + +sonar.issue.ignore.multicriteria=S3437,S4502,S4684,UndocumentedApi +# Rule https://rules.sonarsource.com/java/RSPEC-3437 is ignored, as a JPA-managed field cannot be transient +sonar.issue.ignore.multicriteria.S3437.resourceKey=src/main/java/**/* +sonar.issue.ignore.multicriteria.S3437.ruleKey=squid:S3437 +# Rule https://rules.sonarsource.com/java/RSPEC-1176 is ignored, as we want to follow "clean code" guidelines and classes, methods and arguments names should be self-explanatory +sonar.issue.ignore.multicriteria.UndocumentedApi.resourceKey=src/main/java/**/* +sonar.issue.ignore.multicriteria.UndocumentedApi.ruleKey=squid:UndocumentedApi +# Rule https://rules.sonarsource.com/java/RSPEC-4502 is ignored, as for JWT tokens we are not subject to CSRF attack +sonar.issue.ignore.multicriteria.S4502.resourceKey=src/main/java/**/* +sonar.issue.ignore.multicriteria.S4502.ruleKey=squid:S4502 +# Rule https://rules.sonarsource.com/java/RSPEC-4684 +sonar.issue.ignore.multicriteria.S4684.resourceKey=src/main/java/**/* +sonar.issue.ignore.multicriteria.S4684.ruleKey=java:S4684 diff --git a/src/main/docker/app.yml b/src/main/docker/app.yml new file mode 100644 index 0000000..addd8e5 --- /dev/null +++ b/src/main/docker/app.yml @@ -0,0 +1,29 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +version: '3.8' +services: + store-app: + image: store + environment: + - _JAVA_OPTIONS=-Xmx512m -Xms256m + - SPRING_PROFILES_ACTIVE=prod,api-docs + - MANAGEMENT_METRICS_EXPORT_PROMETHEUS_ENABLED=true + - SPRING_DATASOURCE_URL=jdbc:mysql://store-mysql:3306/store?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=UTC&createDatabaseIfNotExist=true + - SPRING_LIQUIBASE_URL=jdbc:mysql://store-mysql:3306/store?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=UTC&createDatabaseIfNotExist=true + - JHIPSTER_SLEEP=30 # gives time for other services to boot before the application + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:8080:8080 + store-mysql: + image: mysql:8.0.23 + # volumes: + # - ~/volumes/jhipster/store/mysql/:/var/lib/mysql/ + environment: + - MYSQL_USER=root + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=store + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:3306:3306 + command: mysqld --lower_case_table_names=1 --skip-ssl --character_set_server=utf8mb4 --explicit_defaults_for_timestamp diff --git a/src/main/docker/grafana/provisioning/dashboards/JVM.json b/src/main/docker/grafana/provisioning/dashboards/JVM.json new file mode 100644 index 0000000..5104abc --- /dev/null +++ b/src/main/docker/grafana/provisioning/dashboards/JVM.json @@ -0,0 +1,3778 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "limit": 100, + "name": "Annotations & Alerts", + "showIn": 0, + "type": "dashboard" + }, + { + "datasource": "Prometheus", + "enable": true, + "expr": "resets(process_uptime_seconds{application=\"$application\", instance=\"$instance\"}[1m]) > 0", + "iconColor": "rgba(255, 96, 96, 1)", + "name": "Restart Detection", + "showIn": 0, + "step": "1m", + "tagKeys": "restart-tag", + "textFormat": "uptime reset", + "titleFormat": "Restart" + } + ] + }, + "description": "Dashboard for Micrometer instrumented applications (Java, Spring Boot, Micronaut)", + "editable": true, + "gnetId": 4701, + "graphTooltip": 1, + "iteration": 1553765841423, + "links": [], + "panels": [ + { + "content": "\n# Acknowledgments\n\nThank you to [Michael Weirauch](https://twitter.com/emwexx) for creating this dashboard: see original JVM (Micrometer) dashboard at [https://grafana.com/dashboards/4701](https://grafana.com/dashboards/4701)\n\n\n\n", + "gridPos": { + "h": 3, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 141, + "links": [], + "mode": "markdown", + "timeFrom": null, + "timeShift": null, + "title": "Acknowledgments", + "type": "text" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 3 + }, + "id": 125, + "panels": [], + "repeat": null, + "title": "Quick Facts", + "type": "row" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], + "datasource": "Prometheus", + "decimals": 1, + "editable": true, + "error": false, + "format": "s", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 0, + "y": 4 + }, + "height": "", + "id": 63, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "process_uptime_seconds{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "", + "title": "Uptime", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"], + "datasource": "Prometheus", + "decimals": null, + "editable": true, + "error": false, + "format": "dateTimeAsIso", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 6, + "y": 4 + }, + "height": "", + "id": 92, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "process_start_time_seconds{application=\"$application\", instance=\"$instance\"}*1000", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "metric": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "", + "title": "Start time", + "type": "singlestat", + "valueFontSize": "70%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": ["rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)"], + "datasource": "Prometheus", + "decimals": 2, + "editable": true, + "error": false, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 12, + "y": 4 + }, + "id": 65, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})*100/sum(jvm_memory_max_bytes{application=\"$application\",instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "70,90", + "title": "Heap used", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": ["rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)"], + "datasource": "Prometheus", + "decimals": 2, + "editable": true, + "error": false, + "format": "percent", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 3, + "w": 6, + "x": 18, + "y": 4 + }, + "id": 75, + "interval": null, + "links": [], + "mappingType": 2, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "70%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + }, + { + "from": "-99999999999999999999999999999999", + "text": "N/A", + "to": "0" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})*100/sum(jvm_memory_max_bytes{application=\"$application\",instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A", + "step": 14400 + } + ], + "thresholds": "70,90", + "title": "Non-Heap used", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + }, + { + "op": "=", + "text": "x", + "value": "" + } + ], + "valueName": "current" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 126, + "panels": [], + "repeat": null, + "title": "I/O Overview", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 8 + }, + "id": 111, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\"}[1m]))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HTTP", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Rate", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "HTTP": "#890f02", + "HTTP - 5xx": "#bf1b00" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 8 + }, + "id": 112, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\", status=~\"5..\"}[1m]))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HTTP - 5xx", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Errors", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 8 + }, + "id": 113, + "legend": { + "avg": false, + "current": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(rate(http_server_requests_seconds_sum{application=\"$application\", instance=\"$instance\", status!~\"5..\"}[1m]))/sum(rate(http_server_requests_seconds_count{application=\"$application\", instance=\"$instance\", status!~\"5..\"}[1m]))", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "HTTP - AVG", + "refId": "A" + }, + { + "expr": "max(http_server_requests_seconds_max{application=\"$application\", instance=\"$instance\", status!~\"5..\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "HTTP - MAX", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Duration", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 127, + "panels": [], + "repeat": null, + "title": "JVM Memory", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 24, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "JVM Heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 16 + }, + "id": 25, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "JVM Non-Heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 16 + }, + "id": 26, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "sum(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "committed", + "refId": "B", + "step": 2400 + }, + { + "expr": "sum(jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\"})", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "C", + "step": 2400 + }, + { + "expr": "process_memory_vss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": true, + "intervalFactor": 2, + "legendFormat": "vss", + "metric": "", + "refId": "D", + "step": 2400 + }, + { + "expr": "process_memory_rss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "rss", + "refId": "E", + "step": 2400 + }, + { + "expr": "process_memory_pss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "pss", + "refId": "F", + "step": 2400 + }, + { + "expr": "process_memory_swap_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "swap", + "refId": "G", + "step": 2400 + }, + { + "expr": "process_memory_swappss_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "swappss", + "refId": "H", + "step": 2400 + }, + { + "expr": "process_memory_pss_bytes{application=\"$application\", instance=\"$instance\"} + process_memory_swap_bytes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "phys (pss+swap)", + "refId": "I", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "JVM Total", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 128, + "panels": [], + "repeat": null, + "title": "JVM Misc", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 24 + }, + "id": 106, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "system_cpu_usage{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "system", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "process_cpu_usage{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "process", + "refId": "B" + }, + { + "expr": "avg_over_time(process_cpu_usage{application=\"$application\", instance=\"$instance\"}[1h])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "process-1h", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "CPU", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 1, + "format": "percentunit", + "label": "", + "logBase": 1, + "max": "1", + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 24 + }, + "id": 93, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "system_load_average_1m{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "system-1m", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 2, + "refId": "B" + }, + { + "expr": "system_cpu_count{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "cpu", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Load", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 1, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 24 + }, + "id": 32, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_threads_live{application=\"$application\", instance=\"$instance\"} or jvm_threads_live_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "live", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "jvm_threads_daemon{application=\"$application\", instance=\"$instance\"} or jvm_threads_daemon_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "daemon", + "metric": "", + "refId": "B", + "step": 2400 + }, + { + "expr": "jvm_threads_peak{application=\"$application\", instance=\"$instance\"} or jvm_threads_peak_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "peak", + "refId": "C", + "step": 2400 + }, + { + "expr": "process_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "process", + "refId": "D", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Threads", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "blocked": "#bf1b00", + "new": "#fce2de", + "runnable": "#7eb26d", + "terminated": "#511749", + "timed-waiting": "#c15c17", + "waiting": "#eab839" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 24 + }, + "id": 124, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_threads_states_threads{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "{{state}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Thread States", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "debug": "#1F78C1", + "error": "#BF1B00", + "info": "#508642", + "trace": "#6ED0E0", + "warn": "#EAB839" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 18, + "x": 0, + "y": 31 + }, + "height": "", + "id": 91, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": true, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "error", + "yaxis": 1 + }, + { + "alias": "warn", + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "increase(logback_events_total{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{level}}", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Log Events (1m)", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 31 + }, + "id": 61, + "legend": { + "avg": false, + "current": true, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "process_open_fds{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "open", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "process_max_fds{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "B", + "step": 2400 + }, + { + "expr": "process_files_open{application=\"$application\", instance=\"$instance\"} or process_files_open_files{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "open", + "refId": "C" + }, + { + "expr": "process_files_max{application=\"$application\", instance=\"$instance\"} or process_files_max_files{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "max", + "refId": "D" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "File Descriptors", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 10, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "id": 129, + "panels": [], + "repeat": "persistence_counts", + "title": "JVM Memory Pools (Heap)", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 39 + }, + "id": 3, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": "jvm_memory_pool_heap", + "scopedVars": { + "jvm_memory_pool_heap": { + "selected": false, + "text": "PS Eden Space", + "value": "PS Eden Space" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 39 + }, + "id": 134, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "repeatIteration": 1553765841423, + "repeatPanelId": 3, + "scopedVars": { + "jvm_memory_pool_heap": { + "selected": false, + "text": "PS Old Gen", + "value": "PS Old Gen" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 39 + }, + "id": 135, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "repeatIteration": 1553765841423, + "repeatPanelId": 3, + "scopedVars": { + "jvm_memory_pool_heap": { + "selected": false, + "text": "PS Survivor Space", + "value": "PS Survivor Space" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_heap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_heap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 46 + }, + "id": 130, + "panels": [], + "repeat": null, + "title": "JVM Memory Pools (Non-Heap)", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 47 + }, + "id": 78, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": "jvm_memory_pool_nonheap", + "scopedVars": { + "jvm_memory_pool_nonheap": { + "selected": false, + "text": "Metaspace", + "value": "Metaspace" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_nonheap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 47 + }, + "id": 136, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "repeatIteration": 1553765841423, + "repeatPanelId": 78, + "scopedVars": { + "jvm_memory_pool_nonheap": { + "selected": false, + "text": "Compressed Class Space", + "value": "Compressed Class Space" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_nonheap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 47 + }, + "id": 137, + "legend": { + "alignAsTable": false, + "avg": false, + "current": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "maxPerRow": 3, + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "repeat": null, + "repeatIteration": 1553765841423, + "repeatPanelId": 78, + "scopedVars": { + "jvm_memory_pool_nonheap": { + "selected": false, + "text": "Code Cache", + "value": "Code Cache" + } + }, + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 1800 + }, + { + "expr": "jvm_memory_committed_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "commited", + "metric": "", + "refId": "B", + "step": 1800 + }, + { + "expr": "jvm_memory_max_bytes{application=\"$application\", instance=\"$instance\", id=\"$jvm_memory_pool_nonheap\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "max", + "metric": "", + "refId": "C", + "step": 1800 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "$jvm_memory_pool_nonheap", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["mbytes", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 54 + }, + "id": 131, + "panels": [], + "repeat": null, + "title": "Garbage Collection", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 55 + }, + "id": 98, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(jvm_gc_pause_seconds_count{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "hide": false, + "intervalFactor": 2, + "legendFormat": "{{action}} ({{cause}})", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Collections", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "ops", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 55 + }, + "id": 101, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(jvm_gc_pause_seconds_sum{application=\"$application\", instance=\"$instance\"}[1m])/rate(jvm_gc_pause_seconds_count{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "avg {{action}} ({{cause}})", + "refId": "A" + }, + { + "expr": "jvm_gc_pause_seconds_max{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "hide": false, + "instant": false, + "intervalFactor": 1, + "legendFormat": "max {{action}} ({{cause}})", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Pause Durations", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 55 + }, + "id": 99, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(jvm_gc_memory_allocated_bytes_total{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "allocated", + "refId": "A" + }, + { + "expr": "rate(jvm_gc_memory_promoted_bytes_total{application=\"$application\", instance=\"$instance\"}[1m])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "promoted", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Allocated/Promoted", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 62 + }, + "id": 132, + "panels": [], + "repeat": null, + "title": "Classloading", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 63 + }, + "id": 37, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_classes_loaded{application=\"$application\", instance=\"$instance\"} or jvm_classes_loaded_classes{application=\"$application\", instance=\"$instance\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "loaded", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Classes loaded", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 63 + }, + "id": 38, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "delta(jvm_classes_loaded{application=\"$application\",instance=\"$instance\"}[5m]) or delta(jvm_classes_loaded_classes{application=\"$application\",instance=\"$instance\"}[5m])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "delta", + "metric": "", + "refId": "A", + "step": 1200 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Class delta (5m)", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["ops", "short"], + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 70 + }, + "id": 133, + "panels": [], + "repeat": null, + "title": "Buffer Pools", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 71 + }, + "id": 33, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffer_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"direct\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "jvm_buffer_total_capacity_bytes{application=\"$application\", instance=\"$instance\", id=\"direct\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "capacity", + "metric": "", + "refId": "B", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Direct Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 71 + }, + "id": 83, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffer_count{application=\"$application\", instance=\"$instance\", id=\"direct\"} or jvm_buffer_count_buffers{application=\"$application\", instance=\"$instance\", id=\"direct\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "count", + "metric": "", + "refId": "A", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Direct Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 71 + }, + "id": 85, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffer_memory_used_bytes{application=\"$application\", instance=\"$instance\", id=\"mapped\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "used", + "metric": "", + "refId": "A", + "step": 2400 + }, + { + "expr": "jvm_buffer_total_capacity_bytes{application=\"$application\", instance=\"$instance\", id=\"mapped\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "capacity", + "metric": "", + "refId": "B", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Mapped Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 71 + }, + "id": 84, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "paceLength": 10, + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "jvm_buffer_count{application=\"$application\", instance=\"$instance\", id=\"mapped\"} or jvm_buffer_count_buffers{application=\"$application\", instance=\"$instance\", id=\"mapped\"}", + "format": "time_series", + "intervalFactor": 2, + "legendFormat": "count", + "metric": "", + "refId": "A", + "step": 2400 + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Mapped Buffers", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": ["short", "short"], + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": 0, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": "10s", + "schemaVersion": 18, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "allValue": null, + "current": { + "text": "test", + "value": "test" + }, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Application", + "multi": false, + "name": "application", + "options": [], + "query": "label_values(application)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "glob", + "allValue": null, + "current": { + "text": "localhost:8080", + "value": "localhost:8080" + }, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": false, + "label": "Instance", + "multi": false, + "multiFormat": "glob", + "name": "instance", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\"}, instance)", + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "glob", + "allValue": null, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": true, + "label": "JVM Memory Pools Heap", + "multi": false, + "multiFormat": "glob", + "name": "jvm_memory_pool_heap", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"heap\"},id)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allFormat": "glob", + "allValue": null, + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "Prometheus", + "definition": "", + "hide": 0, + "includeAll": true, + "label": "JVM Memory Pools Non-Heap", + "multi": false, + "multiFormat": "glob", + "name": "jvm_memory_pool_nonheap", + "options": [], + "query": "label_values(jvm_memory_used_bytes{application=\"$application\", instance=\"$instance\", area=\"nonheap\"},id)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 2, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "now": true, + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, + "timezone": "browser", + "title": "JVM (Micrometer)", + "uid": "Ud1CFe3iz", + "version": 1 +} diff --git a/src/main/docker/grafana/provisioning/dashboards/dashboard.yml b/src/main/docker/grafana/provisioning/dashboards/dashboard.yml new file mode 100644 index 0000000..4817a83 --- /dev/null +++ b/src/main/docker/grafana/provisioning/dashboards/dashboard.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: 'Prometheus' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /etc/grafana/provisioning/dashboards diff --git a/src/main/docker/grafana/provisioning/datasources/datasource.yml b/src/main/docker/grafana/provisioning/datasources/datasource.yml new file mode 100644 index 0000000..57b2bb3 --- /dev/null +++ b/src/main/docker/grafana/provisioning/datasources/datasource.yml @@ -0,0 +1,50 @@ +apiVersion: 1 + +# list of datasources that should be deleted from the database +deleteDatasources: + - name: Prometheus + orgId: 1 + +# list of datasources to insert/update depending +# whats available in the database +datasources: + # name of the datasource. Required + - name: Prometheus + # datasource type. Required + type: prometheus + # access mode. direct or proxy. Required + access: proxy + # org id. will default to orgId 1 if not specified + orgId: 1 + # url + # On MacOS, replace localhost by host.docker.internal + url: http://localhost:9090 + # database password, if used + password: + # database user, if used + user: + # database name, if used + database: + # enable/disable basic auth + basicAuth: false + # basic auth username + basicAuthUser: admin + # basic auth password + basicAuthPassword: admin + # enable/disable with credentials headers + withCredentials: + # mark as default datasource. Max one per org + isDefault: true + # fields that will be converted to json and stored in json_data + jsonData: + graphiteVersion: '1.1' + tlsAuth: false + tlsAuthWithCACert: false + # json object of data that will be encrypted. + secureJsonData: + tlsCACert: '...' + tlsClientCert: '...' + tlsClientKey: '...' + version: 1 + # allow users to edit datasources from the UI. + editable: true diff --git a/src/main/docker/jhipster-control-center.yml b/src/main/docker/jhipster-control-center.yml new file mode 100644 index 0000000..5f93c7f --- /dev/null +++ b/src/main/docker/jhipster-control-center.yml @@ -0,0 +1,52 @@ +## How to use JHCC docker compose +# To allow JHCC to reach JHipster application from a docker container note that we set the host as host.docker.internal +# To reach the application from a browser, you need to add '127.0.0.1 host.docker.internal' to your hosts file. +### Discovery mode +# JHCC support 3 kinds of discovery mode: Consul, Eureka and static +# In order to use one, please set SPRING_PROFILES_ACTIVE to one (and only one) of this values: consul,eureka,static +### Discovery properties +# According to the discovery mode choose as Spring profile, you have to set the right properties +# please note that current properties are set to run JHCC with default values, personalize them if needed +# and remove those from other modes. You can only have one mode active. +#### Eureka +# - EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://admin:admin@host.docker.internal:8761/eureka/ +#### Consul +# - SPRING_CLOUD_CONSUL_HOST=host.docker.internal +# - SPRING_CLOUD_CONSUL_PORT=8500 +#### Static +# Add instances to "MyApp" +# - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_MYAPP_0_URI=http://host.docker.internal:8081 +# - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_MYAPP_1_URI=http://host.docker.internal:8082 +# Or add a new application named MyNewApp +# - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_MYNEWAPP_0_URI=http://host.docker.internal:8080 +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production + +#### IMPORTANT +# If you choose Consul or Eureka mode: +# Do not forget to remove the prefix "127.0.0.1" in front of their port in order to expose them. +# This is required because JHCC need to communicate with Consul or Eureka. +# - In Consul mode, the ports are in the consul.yml file. +# - In Eureka mode, the ports are in the jhipster-registry.yml file. + +version: '3.8' +services: + jhipster-control-center: + image: 'jhipster/jhipster-control-center:v0.4.1' + command: + - /bin/sh + - -c + # Patch /etc/hosts to support resolving host.docker.internal to the internal IP address used by the host in all OSes + - echo "`ip route | grep default | cut -d ' ' -f3` host.docker.internal" | tee -a /etc/hosts > /dev/null && java -jar /jhipster-control-center.jar + environment: + - _JAVA_OPTIONS=-Xmx512m -Xms256m + - SPRING_PROFILES_ACTIVE=prod,api-docs,static + - JHIPSTER_SLEEP=30 # gives time for other services to boot before the application + - SPRING_SECURITY_USER_PASSWORD=admin + # The token should have the same value than the one declared in you Spring configuration under the jhipster.security.authentication.jwt.base64-secret configuration's entry + - JHIPSTER_SECURITY_AUTHENTICATION_JWT_BASE64_SECRET=NTMyMGZkYWMzYTVmOWMwMWUwOWNhYjllYzM0MTg2ZDZmNzZmMWZhNjcwYjc2ODA2ZGZjMGJlYTc4YzM3YzczMTFiNjBlYjczMWM4NDM3NTM0N2RkOTNiMWJmOWE4YTUxOTkzMzQ0Zjc2MTNmN2U4NjMyZGMzYjRiNzUwZTI2OTA= + - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_STORE_0_URI=http://host.docker.internal:8080 + - LOGGING_FILE_NAME=/tmp/jhipster-control-center.log + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:7419:7419 diff --git a/src/main/docker/jib/entrypoint.sh b/src/main/docker/jib/entrypoint.sh new file mode 100644 index 0000000..f0390ba --- /dev/null +++ b/src/main/docker/jib/entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +echo "The application will start in ${JHIPSTER_SLEEP}s..." && sleep ${JHIPSTER_SLEEP} +exec java ${JAVA_OPTS} -noverify -XX:+AlwaysPreTouch -Djava.security.egd=file:/dev/./urandom -cp /app/resources/:/app/classes/:/app/libs/* "com.adyen.demo.store.StoreApp" "$@" diff --git a/src/main/docker/monitoring.yml b/src/main/docker/monitoring.yml new file mode 100644 index 0000000..ea4fb7c --- /dev/null +++ b/src/main/docker/monitoring.yml @@ -0,0 +1,31 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +version: '3.8' +services: + store-prometheus: + image: prom/prometheus:v2.25.0 + volumes: + - ./prometheus/:/etc/prometheus/ + command: + - '--config.file=/etc/prometheus/prometheus.yml' + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:9090:9090 + # On MacOS, remove next line and replace localhost by host.docker.internal in prometheus/prometheus.yml and + # grafana/provisioning/datasources/datasource.yml + network_mode: 'host' # to test locally running service + store-grafana: + image: grafana/grafana:7.4.3 + volumes: + - ./grafana/provisioning/:/etc/grafana/provisioning/ + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false + - GF_INSTALL_PLUGINS=grafana-piechart-panel + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:3000:3000 + # On MacOS, remove next line and replace localhost by host.docker.internal in prometheus/prometheus.yml and + # grafana/provisioning/datasources/datasource.yml + network_mode: 'host' # to test locally running service diff --git a/src/main/docker/mysql.yml b/src/main/docker/mysql.yml new file mode 100644 index 0000000..64d9927 --- /dev/null +++ b/src/main/docker/mysql.yml @@ -0,0 +1,15 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +version: '3.8' +services: + store-mysql: + image: mysql:8.0.23 + # volumes: + # - ~/volumes/jhipster/store/mysql/:/var/lib/mysql/ + environment: + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + - MYSQL_DATABASE=store + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:3306:3306 + command: mysqld --lower_case_table_names=1 --skip-ssl --character_set_server=utf8mb4 --explicit_defaults_for_timestamp diff --git a/src/main/docker/prometheus/prometheus.yml b/src/main/docker/prometheus/prometheus.yml new file mode 100644 index 0000000..b370a2f --- /dev/null +++ b/src/main/docker/prometheus/prometheus.yml @@ -0,0 +1,31 @@ +# Sample global config for monitoring JHipster applications +global: + scrape_interval: 15s # By default, scrape targets every 15 seconds. + evaluation_interval: 15s # By default, scrape targets every 15 seconds. + # scrape_timeout is set to the global default (10s). + + # Attach these labels to any time series or alerts when communicating with + # external systems (federation, remote storage, Alertmanager). + external_labels: + monitor: 'jhipster' + +# A scrape configuration containing exactly one endpoint to scrape: +# Here it's Prometheus itself. +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'prometheus' + + # Override the global default and scrape targets from this job every 5 seconds. + scrape_interval: 5s + + # scheme defaults to 'http' enable https in case your application is server via https + #scheme: https + # basic auth is not needed by default. See https://www.jhipster.tech/monitoring/#configuring-metrics-forwarding for details + #basic_auth: + # username: admin + # password: admin + metrics_path: /management/prometheus + static_configs: + - targets: + # On MacOS, replace localhost by host.docker.internal + - localhost:8080 diff --git a/src/main/docker/sonar.yml b/src/main/docker/sonar.yml new file mode 100644 index 0000000..00a1b74 --- /dev/null +++ b/src/main/docker/sonar.yml @@ -0,0 +1,13 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +version: '3.8' +services: + store-sonar: + image: sonarqube:8.7.0-community + # Authentication is turned off for out of the box experience while trying out SonarQube + # For real use cases delete sonar.forceAuthentication variable or set sonar.forceAuthentication=true + environment: + - sonar.forceAuthentication=false + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:9001:9000 diff --git a/src/main/java/com/adyen/demo/store/ApplicationWebXml.java b/src/main/java/com/adyen/demo/store/ApplicationWebXml.java new file mode 100644 index 0000000..cff6d5e --- /dev/null +++ b/src/main/java/com/adyen/demo/store/ApplicationWebXml.java @@ -0,0 +1,19 @@ +package com.adyen.demo.store; + +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import tech.jhipster.config.DefaultProfileUtil; + +/** + * This is a helper Java class that provides an alternative to creating a {@code web.xml}. + * This will be invoked only when the application is deployed to a Servlet container like Tomcat, JBoss etc. + */ +public class ApplicationWebXml extends SpringBootServletInitializer { + + @Override + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { + // set a default to use when no profile is configured. + DefaultProfileUtil.addDefaultProfile(application.application()); + return application.sources(StoreApp.class); + } +} diff --git a/src/main/java/com/adyen/demo/store/GeneratedByJHipster.java b/src/main/java/com/adyen/demo/store/GeneratedByJHipster.java new file mode 100644 index 0000000..ae6178b --- /dev/null +++ b/src/main/java/com/adyen/demo/store/GeneratedByJHipster.java @@ -0,0 +1,13 @@ +package com.adyen.demo.store; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.annotation.Generated; + +@Generated(value = "JHipster", comments = "Generated by JHipster 7.0.0") +@Retention(RetentionPolicy.SOURCE) +@Target({ ElementType.TYPE }) +public @interface GeneratedByJHipster { +} diff --git a/src/main/java/com/adyen/demo/store/StoreApp.java b/src/main/java/com/adyen/demo/store/StoreApp.java new file mode 100644 index 0000000..a6feb47 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/StoreApp.java @@ -0,0 +1,103 @@ +package com.adyen.demo.store; + +import com.adyen.demo.store.config.ApplicationProperties; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; +import javax.annotation.PostConstruct; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.core.env.Environment; +import tech.jhipster.config.DefaultProfileUtil; +import tech.jhipster.config.JHipsterConstants; + +@SpringBootApplication +@EnableConfigurationProperties({ LiquibaseProperties.class, ApplicationProperties.class }) +public class StoreApp { + + private static final Logger log = LoggerFactory.getLogger(StoreApp.class); + + private final Environment env; + + public StoreApp(Environment env) { + this.env = env; + } + + /** + * Initializes store. + *

+ * Spring profiles can be configured with a program argument --spring.profiles.active=your-active-profile + *

+ * You can find more information on how profiles work with JHipster on https://www.jhipster.tech/profiles/. + */ + @PostConstruct + public void initApplication() { + Collection activeProfiles = Arrays.asList(env.getActiveProfiles()); + if ( + activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) && + activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_PRODUCTION) + ) { + log.error( + "You have misconfigured your application! It should not run " + "with both the 'dev' and 'prod' profiles at the same time." + ); + } + if ( + activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) && + activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_CLOUD) + ) { + log.error( + "You have misconfigured your application! It should not " + "run with both the 'dev' and 'cloud' profiles at the same time." + ); + } + } + + /** + * Main method, used to run the application. + * + * @param args the command line arguments. + */ + public static void main(String[] args) { + SpringApplication app = new SpringApplication(StoreApp.class); + DefaultProfileUtil.addDefaultProfile(app); + Environment env = app.run(args).getEnvironment(); + logApplicationStartup(env); + } + + private static void logApplicationStartup(Environment env) { + String protocol = Optional.ofNullable(env.getProperty("server.ssl.key-store")).map(key -> "https").orElse("http"); + String serverPort = env.getProperty("server.port"); + String contextPath = Optional + .ofNullable(env.getProperty("server.servlet.context-path")) + .filter(StringUtils::isNotBlank) + .orElse("/"); + String hostAddress = "localhost"; + try { + hostAddress = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + log.warn("The host name could not be determined, using `localhost` as fallback"); + } + log.info( + "\n----------------------------------------------------------\n\t" + + "Application '{}' is running! Access URLs:\n\t" + + "Local: \t\t{}://localhost:{}{}\n\t" + + "External: \t{}://{}:{}{}\n\t" + + "Profile(s): \t{}\n----------------------------------------------------------", + env.getProperty("spring.application.name"), + protocol, + serverPort, + contextPath, + protocol, + hostAddress, + serverPort, + contextPath, + env.getActiveProfiles() + ); + } +} diff --git a/src/main/java/com/adyen/demo/store/aop/logging/LoggingAspect.java b/src/main/java/com/adyen/demo/store/aop/logging/LoggingAspect.java new file mode 100644 index 0000000..ce69255 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/aop/logging/LoggingAspect.java @@ -0,0 +1,115 @@ +package com.adyen.demo.store.aop.logging; + +import java.util.Arrays; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.AfterThrowing; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; +import tech.jhipster.config.JHipsterConstants; + +/** + * Aspect for logging execution of service and repository Spring components. + * + * By default, it only runs with the "dev" profile. + */ +@Aspect +public class LoggingAspect { + + private final Environment env; + + public LoggingAspect(Environment env) { + this.env = env; + } + + /** + * Pointcut that matches all repositories, services and Web REST endpoints. + */ + @Pointcut( + "within(@org.springframework.stereotype.Repository *)" + + " || within(@org.springframework.stereotype.Service *)" + + " || within(@org.springframework.web.bind.annotation.RestController *)" + ) + public void springBeanPointcut() { + // Method is empty as this is just a Pointcut, the implementations are in the advices. + } + + /** + * Pointcut that matches all Spring beans in the application's main packages. + */ + @Pointcut( + "within(com.adyen.demo.store.repository..*)" + + " || within(com.adyen.demo.store.service..*)" + + " || within(com.adyen.demo.store.web.rest..*)" + ) + public void applicationPackagePointcut() { + // Method is empty as this is just a Pointcut, the implementations are in the advices. + } + + /** + * Retrieves the {@link Logger} associated to the given {@link JoinPoint}. + * + * @param joinPoint join point we want the logger for. + * @return {@link Logger} associated to the given {@link JoinPoint}. + */ + private Logger logger(JoinPoint joinPoint) { + return LoggerFactory.getLogger(joinPoint.getSignature().getDeclaringTypeName()); + } + + /** + * Advice that logs methods throwing exceptions. + * + * @param joinPoint join point for advice. + * @param e exception. + */ + @AfterThrowing(pointcut = "applicationPackagePointcut() && springBeanPointcut()", throwing = "e") + public void logAfterThrowing(JoinPoint joinPoint, Throwable e) { + if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) { + logger(joinPoint) + .error( + "Exception in {}() with cause = \'{}\' and exception = \'{}\'", + joinPoint.getSignature().getName(), + e.getCause() != null ? e.getCause() : "NULL", + e.getMessage(), + e + ); + } else { + logger(joinPoint) + .error( + "Exception in {}() with cause = {}", + joinPoint.getSignature().getName(), + e.getCause() != null ? e.getCause() : "NULL" + ); + } + } + + /** + * Advice that logs when a method is entered and exited. + * + * @param joinPoint join point for advice. + * @return result. + * @throws Throwable throws {@link IllegalArgumentException}. + */ + @Around("applicationPackagePointcut() && springBeanPointcut()") + public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { + Logger log = logger(joinPoint); + if (log.isDebugEnabled()) { + log.debug("Enter: {}() with argument[s] = {}", joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs())); + } + try { + Object result = joinPoint.proceed(); + if (log.isDebugEnabled()) { + log.debug("Exit: {}() with result = {}", joinPoint.getSignature().getName(), result); + } + return result; + } catch (IllegalArgumentException e) { + log.error("Illegal argument: {} in {}()", Arrays.toString(joinPoint.getArgs()), joinPoint.getSignature().getName()); + throw e; + } + } +} diff --git a/src/main/java/com/adyen/demo/store/config/ApplicationProperties.java b/src/main/java/com/adyen/demo/store/config/ApplicationProperties.java new file mode 100644 index 0000000..8d928e9 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/ApplicationProperties.java @@ -0,0 +1,12 @@ +package com.adyen.demo.store.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Properties specific to Store. + *

+ * Properties are configured in the {@code application.yml} file. + * See {@link tech.jhipster.config.JHipsterProperties} for a good example. + */ +@ConfigurationProperties(prefix = "application", ignoreUnknownFields = false) +public class ApplicationProperties {} diff --git a/src/main/java/com/adyen/demo/store/config/AsyncConfiguration.java b/src/main/java/com/adyen/demo/store/config/AsyncConfiguration.java new file mode 100644 index 0000000..35aa19e --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/AsyncConfiguration.java @@ -0,0 +1,46 @@ +package com.adyen.demo.store.config; + +import java.util.concurrent.Executor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; +import org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler; +import org.springframework.boot.autoconfigure.task.TaskExecutionProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import tech.jhipster.async.ExceptionHandlingAsyncTaskExecutor; + +@Configuration +@EnableAsync +@EnableScheduling +public class AsyncConfiguration implements AsyncConfigurer { + + private final Logger log = LoggerFactory.getLogger(AsyncConfiguration.class); + + private final TaskExecutionProperties taskExecutionProperties; + + public AsyncConfiguration(TaskExecutionProperties taskExecutionProperties) { + this.taskExecutionProperties = taskExecutionProperties; + } + + @Override + @Bean(name = "taskExecutor") + public Executor getAsyncExecutor() { + log.debug("Creating Async Task Executor"); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(taskExecutionProperties.getPool().getCoreSize()); + executor.setMaxPoolSize(taskExecutionProperties.getPool().getMaxSize()); + executor.setQueueCapacity(taskExecutionProperties.getPool().getQueueCapacity()); + executor.setThreadNamePrefix(taskExecutionProperties.getThreadNamePrefix()); + return new ExceptionHandlingAsyncTaskExecutor(executor); + } + + @Override + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + return new SimpleAsyncUncaughtExceptionHandler(); + } +} diff --git a/src/main/java/com/adyen/demo/store/config/CacheConfiguration.java b/src/main/java/com/adyen/demo/store/config/CacheConfiguration.java new file mode 100644 index 0000000..3a773a5 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/CacheConfiguration.java @@ -0,0 +1,86 @@ +package com.adyen.demo.store.config; + +import java.time.Duration; +import org.ehcache.config.builders.*; +import org.ehcache.jsr107.Eh107Configuration; +import org.hibernate.cache.jcache.ConfigSettings; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer; +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import org.springframework.boot.info.BuildProperties; +import org.springframework.boot.info.GitProperties; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.context.annotation.*; +import tech.jhipster.config.JHipsterProperties; +import tech.jhipster.config.cache.PrefixedKeyGenerator; + +@Configuration +@EnableCaching +public class CacheConfiguration { + + private GitProperties gitProperties; + private BuildProperties buildProperties; + private final javax.cache.configuration.Configuration jcacheConfiguration; + + public CacheConfiguration(JHipsterProperties jHipsterProperties) { + JHipsterProperties.Cache.Ehcache ehcache = jHipsterProperties.getCache().getEhcache(); + + jcacheConfiguration = + Eh107Configuration.fromEhcacheCacheConfiguration( + CacheConfigurationBuilder + .newCacheConfigurationBuilder(Object.class, Object.class, ResourcePoolsBuilder.heap(ehcache.getMaxEntries())) + .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(ehcache.getTimeToLiveSeconds()))) + .build() + ); + } + + @Bean + public HibernatePropertiesCustomizer hibernatePropertiesCustomizer(javax.cache.CacheManager cacheManager) { + return hibernateProperties -> hibernateProperties.put(ConfigSettings.CACHE_MANAGER, cacheManager); + } + + @Bean + public JCacheManagerCustomizer cacheManagerCustomizer() { + return cm -> { + createCache(cm, com.adyen.demo.store.repository.UserRepository.USERS_BY_LOGIN_CACHE); + createCache(cm, com.adyen.demo.store.repository.UserRepository.USERS_BY_EMAIL_CACHE); + createCache(cm, com.adyen.demo.store.domain.User.class.getName()); + createCache(cm, com.adyen.demo.store.domain.Authority.class.getName()); + createCache(cm, com.adyen.demo.store.domain.User.class.getName() + ".authorities"); + createCache(cm, com.adyen.demo.store.domain.Product.class.getName()); + createCache(cm, com.adyen.demo.store.domain.ProductCategory.class.getName()); + createCache(cm, com.adyen.demo.store.domain.ProductCategory.class.getName() + ".products"); + createCache(cm, com.adyen.demo.store.domain.CustomerDetails.class.getName()); + createCache(cm, com.adyen.demo.store.domain.CustomerDetails.class.getName() + ".carts"); + createCache(cm, com.adyen.demo.store.domain.ShoppingCart.class.getName()); + createCache(cm, com.adyen.demo.store.domain.ShoppingCart.class.getName() + ".orders"); + createCache(cm, com.adyen.demo.store.domain.ProductOrder.class.getName()); + // jhipster-needle-ehcache-add-entry + }; + } + + private void createCache(javax.cache.CacheManager cm, String cacheName) { + javax.cache.Cache cache = cm.getCache(cacheName); + if (cache != null) { + cache.clear(); + } else { + cm.createCache(cacheName, jcacheConfiguration); + } + } + + @Autowired(required = false) + public void setGitProperties(GitProperties gitProperties) { + this.gitProperties = gitProperties; + } + + @Autowired(required = false) + public void setBuildProperties(BuildProperties buildProperties) { + this.buildProperties = buildProperties; + } + + @Bean + public KeyGenerator keyGenerator() { + return new PrefixedKeyGenerator(this.gitProperties, this.buildProperties); + } +} diff --git a/src/main/java/com/adyen/demo/store/config/Constants.java b/src/main/java/com/adyen/demo/store/config/Constants.java new file mode 100644 index 0000000..746006c --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/Constants.java @@ -0,0 +1,15 @@ +package com.adyen.demo.store.config; + +/** + * Application constants. + */ +public final class Constants { + + // Regex for acceptable logins + public static final String LOGIN_REGEX = "^(?>[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*)|(?>[_.@A-Za-z0-9-]+)$"; + + public static final String SYSTEM = "system"; + public static final String DEFAULT_LANGUAGE = "en"; + + private Constants() {} +} diff --git a/src/main/java/com/adyen/demo/store/config/DatabaseConfiguration.java b/src/main/java/com/adyen/demo/store/config/DatabaseConfiguration.java new file mode 100644 index 0000000..cd366b3 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/DatabaseConfiguration.java @@ -0,0 +1,57 @@ +package com.adyen.demo.store.config; + +import java.sql.SQLException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.Environment; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.config.h2.H2ConfigurationHelper; + +@Configuration +@EnableJpaRepositories("com.adyen.demo.store.repository") +@EnableJpaAuditing(auditorAwareRef = "springSecurityAuditorAware") +@EnableTransactionManagement +public class DatabaseConfiguration { + + private final Logger log = LoggerFactory.getLogger(DatabaseConfiguration.class); + + private final Environment env; + + public DatabaseConfiguration(Environment env) { + this.env = env; + } + + /** + * Open the TCP port for the H2 database, so it is available remotely. + * + * @return the H2 database TCP server. + * @throws SQLException if the server failed to start. + */ + @Bean(initMethod = "start", destroyMethod = "stop") + @Profile(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) + public Object h2TCPServer() throws SQLException { + String port = getValidPortForH2(); + log.debug("H2 database is available on port {}", port); + return H2ConfigurationHelper.createServer(port); + } + + private String getValidPortForH2() { + int port = Integer.parseInt(env.getProperty("server.port")); + if (port < 10000) { + port = 10000 + port; + } else { + if (port < 63536) { + port = port + 2000; + } else { + port = port - 2000; + } + } + return String.valueOf(port); + } +} diff --git a/src/main/java/com/adyen/demo/store/config/DateTimeFormatConfiguration.java b/src/main/java/com/adyen/demo/store/config/DateTimeFormatConfiguration.java new file mode 100644 index 0000000..809e499 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/DateTimeFormatConfiguration.java @@ -0,0 +1,20 @@ +package com.adyen.demo.store.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * Configure the converters to use the ISO format for dates by default. + */ +@Configuration +public class DateTimeFormatConfiguration implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setUseIsoFormat(true); + registrar.registerFormatters(registry); + } +} diff --git a/src/main/java/com/adyen/demo/store/config/JacksonConfiguration.java b/src/main/java/com/adyen/demo/store/config/JacksonConfiguration.java new file mode 100644 index 0000000..fc0d059 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/JacksonConfiguration.java @@ -0,0 +1,51 @@ +package com.adyen.demo.store.config; + +import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.zalando.problem.ProblemModule; +import org.zalando.problem.violations.ConstraintViolationProblemModule; + +@Configuration +public class JacksonConfiguration { + + /** + * Support for Java date and time API. + * @return the corresponding Jackson module. + */ + @Bean + public JavaTimeModule javaTimeModule() { + return new JavaTimeModule(); + } + + @Bean + public Jdk8Module jdk8TimeModule() { + return new Jdk8Module(); + } + + /* + * Support for Hibernate types in Jackson. + */ + @Bean + public Hibernate5Module hibernate5Module() { + return new Hibernate5Module(); + } + + /* + * Module for serialization/deserialization of RFC7807 Problem. + */ + @Bean + public ProblemModule problemModule() { + return new ProblemModule(); + } + + /* + * Module for serialization/deserialization of ConstraintViolationProblem. + */ + @Bean + public ConstraintViolationProblemModule constraintViolationProblemModule() { + return new ConstraintViolationProblemModule(); + } +} diff --git a/src/main/java/com/adyen/demo/store/config/LiquibaseConfiguration.java b/src/main/java/com/adyen/demo/store/config/LiquibaseConfiguration.java new file mode 100644 index 0000000..cee9998 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/LiquibaseConfiguration.java @@ -0,0 +1,69 @@ +package com.adyen.demo.store.config; + +import java.util.concurrent.Executor; +import javax.sql.DataSource; +import liquibase.integration.spring.SpringLiquibase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseDataSource; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.config.liquibase.SpringLiquibaseUtil; + +@Configuration +public class LiquibaseConfiguration { + + private final Logger log = LoggerFactory.getLogger(LiquibaseConfiguration.class); + + private final Environment env; + + public LiquibaseConfiguration(Environment env) { + this.env = env; + } + + @Bean + public SpringLiquibase liquibase( + @Qualifier("taskExecutor") Executor executor, + @LiquibaseDataSource ObjectProvider liquibaseDataSource, + LiquibaseProperties liquibaseProperties, + ObjectProvider dataSource, + DataSourceProperties dataSourceProperties + ) { + // If you don't want Liquibase to start asynchronously, substitute by this: + // SpringLiquibase liquibase = SpringLiquibaseUtil.createSpringLiquibase(liquibaseDataSource.getIfAvailable(), liquibaseProperties, dataSource.getIfUnique(), dataSourceProperties); + SpringLiquibase liquibase = SpringLiquibaseUtil.createAsyncSpringLiquibase( + this.env, + executor, + liquibaseDataSource.getIfAvailable(), + liquibaseProperties, + dataSource.getIfUnique(), + dataSourceProperties + ); + liquibase.setChangeLog("classpath:config/liquibase/master.xml"); + liquibase.setContexts(liquibaseProperties.getContexts()); + liquibase.setDefaultSchema(liquibaseProperties.getDefaultSchema()); + liquibase.setLiquibaseSchema(liquibaseProperties.getLiquibaseSchema()); + liquibase.setLiquibaseTablespace(liquibaseProperties.getLiquibaseTablespace()); + liquibase.setDatabaseChangeLogLockTable(liquibaseProperties.getDatabaseChangeLogLockTable()); + liquibase.setDatabaseChangeLogTable(liquibaseProperties.getDatabaseChangeLogTable()); + liquibase.setDropFirst(liquibaseProperties.isDropFirst()); + liquibase.setLabels(liquibaseProperties.getLabels()); + liquibase.setChangeLogParameters(liquibaseProperties.getParameters()); + liquibase.setRollbackFile(liquibaseProperties.getRollbackFile()); + liquibase.setTestRollbackOnUpdate(liquibaseProperties.isTestRollbackOnUpdate()); + if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_NO_LIQUIBASE))) { + liquibase.setShouldRun(false); + } else { + liquibase.setShouldRun(liquibaseProperties.isEnabled()); + log.debug("Configuring Liquibase"); + } + return liquibase; + } +} diff --git a/src/main/java/com/adyen/demo/store/config/LocaleConfiguration.java b/src/main/java/com/adyen/demo/store/config/LocaleConfiguration.java new file mode 100644 index 0000000..7536eec --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/LocaleConfiguration.java @@ -0,0 +1,26 @@ +package com.adyen.demo.store.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.*; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; +import tech.jhipster.config.locale.AngularCookieLocaleResolver; + +@Configuration +public class LocaleConfiguration implements WebMvcConfigurer { + + @Bean + public LocaleResolver localeResolver() { + AngularCookieLocaleResolver cookieLocaleResolver = new AngularCookieLocaleResolver(); + cookieLocaleResolver.setCookieName("NG_TRANSLATE_LANG_KEY"); + return cookieLocaleResolver; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor(); + localeChangeInterceptor.setParamName("language"); + registry.addInterceptor(localeChangeInterceptor); + } +} diff --git a/src/main/java/com/adyen/demo/store/config/LoggingAspectConfiguration.java b/src/main/java/com/adyen/demo/store/config/LoggingAspectConfiguration.java new file mode 100644 index 0000000..b97704d --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/LoggingAspectConfiguration.java @@ -0,0 +1,17 @@ +package com.adyen.demo.store.config; + +import com.adyen.demo.store.aop.logging.LoggingAspect; +import org.springframework.context.annotation.*; +import org.springframework.core.env.Environment; +import tech.jhipster.config.JHipsterConstants; + +@Configuration +@EnableAspectJAutoProxy +public class LoggingAspectConfiguration { + + @Bean + @Profile(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT) + public LoggingAspect loggingAspect(Environment env) { + return new LoggingAspect(env); + } +} diff --git a/src/main/java/com/adyen/demo/store/config/LoggingConfiguration.java b/src/main/java/com/adyen/demo/store/config/LoggingConfiguration.java new file mode 100644 index 0000000..0a23430 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/LoggingConfiguration.java @@ -0,0 +1,47 @@ +package com.adyen.demo.store.config; + +import static tech.jhipster.config.logging.LoggingUtils.*; + +import ch.qos.logback.classic.LoggerContext; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.HashMap; +import java.util.Map; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import tech.jhipster.config.JHipsterProperties; + +/* + * Configures the console and Logstash log appenders from the app properties + */ +@Configuration +public class LoggingConfiguration { + + public LoggingConfiguration( + @Value("${spring.application.name}") String appName, + @Value("${server.port}") String serverPort, + JHipsterProperties jHipsterProperties, + ObjectMapper mapper + ) throws JsonProcessingException { + LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); + + Map map = new HashMap<>(); + map.put("app_name", appName); + map.put("app_port", serverPort); + String customFields = mapper.writeValueAsString(map); + + JHipsterProperties.Logging loggingProperties = jHipsterProperties.getLogging(); + JHipsterProperties.Logging.Logstash logstashProperties = loggingProperties.getLogstash(); + + if (loggingProperties.isUseJsonFormat()) { + addJsonConsoleAppender(context, customFields); + } + if (logstashProperties.isEnabled()) { + addLogstashTcpSocketAppender(context, customFields, logstashProperties); + } + if (loggingProperties.isUseJsonFormat() || logstashProperties.isEnabled()) { + addContextListener(context, customFields, loggingProperties); + } + } +} diff --git a/src/main/java/com/adyen/demo/store/config/SecurityConfiguration.java b/src/main/java/com/adyen/demo/store/config/SecurityConfiguration.java new file mode 100644 index 0000000..ffdde17 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/SecurityConfiguration.java @@ -0,0 +1,102 @@ +package com.adyen.demo.store.config; + +import com.adyen.demo.store.security.*; +import com.adyen.demo.store.security.jwt.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; +import org.springframework.web.filter.CorsFilter; +import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport; + +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +@Import(SecurityProblemSupport.class) +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + private final TokenProvider tokenProvider; + + private final CorsFilter corsFilter; + private final SecurityProblemSupport problemSupport; + + public SecurityConfiguration(TokenProvider tokenProvider, CorsFilter corsFilter, SecurityProblemSupport problemSupport) { + this.tokenProvider = tokenProvider; + this.corsFilter = corsFilter; + this.problemSupport = problemSupport; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Override + public void configure(WebSecurity web) { + web + .ignoring() + .antMatchers(HttpMethod.OPTIONS, "/**") + .antMatchers("/app/**/*.{js,html}") + .antMatchers("/i18n/**") + .antMatchers("/content/**") + .antMatchers("/h2-console/**") + .antMatchers("/swagger-ui/**") + .antMatchers("/test/**"); + } + + @Override + public void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf() + .disable() + .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling() + .authenticationEntryPoint(problemSupport) + .accessDeniedHandler(problemSupport) + .and() + .headers() + .contentSecurityPolicy("default-src 'self'; frame-src 'self' data:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://storage.googleapis.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:") + .and() + .referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN) + .and() + .featurePolicy("geolocation 'none'; midi 'none'; sync-xhr 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; fullscreen 'self'; payment 'none'") + .and() + .frameOptions() + .deny() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/api/authenticate").permitAll() + .antMatchers("/api/register").permitAll() + .antMatchers("/api/activate").permitAll() + .antMatchers("/api/account/reset-password/init").permitAll() + .antMatchers("/api/account/reset-password/finish").permitAll() + .antMatchers("/api/admin/**").hasAuthority(AuthoritiesConstants.ADMIN) + .antMatchers("/api/**").authenticated() + .antMatchers("/management/health").permitAll() + .antMatchers("/management/health/**").permitAll() + .antMatchers("/management/info").permitAll() + .antMatchers("/management/prometheus").permitAll() + .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN) + .and() + .httpBasic() + .and() + .apply(securityConfigurerAdapter()); + // @formatter:on + } + + private JWTConfigurer securityConfigurerAdapter() { + return new JWTConfigurer(tokenProvider); + } +} diff --git a/src/main/java/com/adyen/demo/store/config/StaticResourcesWebConfiguration.java b/src/main/java/com/adyen/demo/store/config/StaticResourcesWebConfiguration.java new file mode 100644 index 0000000..d4b3988 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/StaticResourcesWebConfiguration.java @@ -0,0 +1,51 @@ +package com.adyen.demo.store.config; + +import java.util.concurrent.TimeUnit; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.CacheControl; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.config.JHipsterProperties; + +@Configuration +@Profile({ JHipsterConstants.SPRING_PROFILE_PRODUCTION }) +public class StaticResourcesWebConfiguration implements WebMvcConfigurer { + + protected static final String[] RESOURCE_LOCATIONS = new String[] { + "classpath:/static/app/", + "classpath:/static/content/", + "classpath:/static/i18n/", + }; + protected static final String[] RESOURCE_PATHS = new String[] { "/app/*", "/content/*", "/i18n/*" }; + + private final JHipsterProperties jhipsterProperties; + + public StaticResourcesWebConfiguration(JHipsterProperties jHipsterProperties) { + this.jhipsterProperties = jHipsterProperties; + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + ResourceHandlerRegistration resourceHandlerRegistration = appendResourceHandler(registry); + initializeResourceHandler(resourceHandlerRegistration); + } + + protected ResourceHandlerRegistration appendResourceHandler(ResourceHandlerRegistry registry) { + return registry.addResourceHandler(RESOURCE_PATHS); + } + + protected void initializeResourceHandler(ResourceHandlerRegistration resourceHandlerRegistration) { + resourceHandlerRegistration.addResourceLocations(RESOURCE_LOCATIONS).setCacheControl(getCacheControl()); + } + + protected CacheControl getCacheControl() { + return CacheControl.maxAge(getJHipsterHttpCacheProperty(), TimeUnit.DAYS).cachePublic(); + } + + private int getJHipsterHttpCacheProperty() { + return jhipsterProperties.getHttp().getCache().getTimeToLiveInDays(); + } +} diff --git a/src/main/java/com/adyen/demo/store/config/WebConfigurer.java b/src/main/java/com/adyen/demo/store/config/WebConfigurer.java new file mode 100644 index 0000000..d144155 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/WebConfigurer.java @@ -0,0 +1,121 @@ +package com.adyen.demo.store.config; + +import static java.net.URLDecoder.decode; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.*; +import javax.servlet.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.server.*; +import org.springframework.boot.web.servlet.ServletContextInitializer; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; +import org.springframework.util.CollectionUtils; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.config.JHipsterProperties; +import tech.jhipster.config.h2.H2ConfigurationHelper; + +/** + * Configuration of web application with Servlet 3.0 APIs. + */ +@Configuration +public class WebConfigurer implements ServletContextInitializer, WebServerFactoryCustomizer { + + private final Logger log = LoggerFactory.getLogger(WebConfigurer.class); + + private final Environment env; + + private final JHipsterProperties jHipsterProperties; + + public WebConfigurer(Environment env, JHipsterProperties jHipsterProperties) { + this.env = env; + this.jHipsterProperties = jHipsterProperties; + } + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + if (env.getActiveProfiles().length != 0) { + log.info("Web application configuration, using profiles: {}", (Object[]) env.getActiveProfiles()); + } + + if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) { + initH2Console(servletContext); + } + log.info("Web application fully configured"); + } + + /** + * Customize the Servlet engine: Mime types, the document root, the cache. + */ + @Override + public void customize(WebServerFactory server) { + // When running in an IDE or with ./gradlew bootRun, set location of the static web assets. + setLocationForStaticAssets(server); + } + + private void setLocationForStaticAssets(WebServerFactory server) { + if (server instanceof ConfigurableServletWebServerFactory) { + ConfigurableServletWebServerFactory servletWebServer = (ConfigurableServletWebServerFactory) server; + File root; + String prefixPath = resolvePathPrefix(); + root = new File(prefixPath + "build/resources/main/static/"); + if (root.exists() && root.isDirectory()) { + servletWebServer.setDocumentRoot(root); + } + } + } + + /** + * Resolve path prefix to static resources. + */ + private String resolvePathPrefix() { + String fullExecutablePath; + try { + fullExecutablePath = decode(this.getClass().getResource("").getPath(), StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + /* try without decoding if this ever happens */ + fullExecutablePath = this.getClass().getResource("").getPath(); + } + String rootPath = Paths.get(".").toUri().normalize().getPath(); + String extractedPath = fullExecutablePath.replace(rootPath, ""); + int extractionEndIndex = extractedPath.indexOf("build/"); + if (extractionEndIndex <= 0) { + return ""; + } + return extractedPath.substring(0, extractionEndIndex); + } + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = jHipsterProperties.getCors(); + if (!CollectionUtils.isEmpty(config.getAllowedOrigins())) { + log.debug("Registering CORS filter"); + source.registerCorsConfiguration("/api/**", config); + source.registerCorsConfiguration("/management/**", config); + source.registerCorsConfiguration("/v2/api-docs", config); + source.registerCorsConfiguration("/v3/api-docs", config); + source.registerCorsConfiguration("/swagger-resources", config); + source.registerCorsConfiguration("/swagger-ui/**", config); + } + return new CorsFilter(source); + } + + /** + * Initializes H2 console. + */ + private void initH2Console(ServletContext servletContext) { + log.debug("Initialize H2 console"); + H2ConfigurationHelper.initH2Console(servletContext); + } +} diff --git a/src/main/java/com/adyen/demo/store/config/package-info.java b/src/main/java/com/adyen/demo/store/config/package-info.java new file mode 100644 index 0000000..3a74db3 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/config/package-info.java @@ -0,0 +1,4 @@ +/** + * Spring Framework configuration files. + */ +package com.adyen.demo.store.config; diff --git a/src/main/java/com/adyen/demo/store/domain/AbstractAuditingEntity.java b/src/main/java/com/adyen/demo/store/domain/AbstractAuditingEntity.java new file mode 100644 index 0000000..0cd45ad --- /dev/null +++ b/src/main/java/com/adyen/demo/store/domain/AbstractAuditingEntity.java @@ -0,0 +1,76 @@ +package com.adyen.demo.store.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.io.Serializable; +import java.time.Instant; +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +/** + * Base abstract class for entities which will hold definitions for created, last modified, created by, + * last modified by attributes. + */ +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class AbstractAuditingEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @CreatedBy + @Column(name = "created_by", nullable = false, length = 50, updatable = false) + @JsonIgnore + private String createdBy; + + @CreatedDate + @Column(name = "created_date", updatable = false) + @JsonIgnore + private Instant createdDate = Instant.now(); + + @LastModifiedBy + @Column(name = "last_modified_by", length = 50) + @JsonIgnore + private String lastModifiedBy; + + @LastModifiedDate + @Column(name = "last_modified_date") + @JsonIgnore + private Instant lastModifiedDate = Instant.now(); + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public Instant getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(Instant createdDate) { + this.createdDate = createdDate; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + + public void setLastModifiedBy(String lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + public Instant getLastModifiedDate() { + return lastModifiedDate; + } + + public void setLastModifiedDate(Instant lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + } +} diff --git a/src/main/java/com/adyen/demo/store/domain/Authority.java b/src/main/java/com/adyen/demo/store/domain/Authority.java new file mode 100644 index 0000000..8cb080f --- /dev/null +++ b/src/main/java/com/adyen/demo/store/domain/Authority.java @@ -0,0 +1,61 @@ +package com.adyen.demo.store.domain; + +import java.io.Serializable; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * An authority (a security role) used by Spring Security. + */ +@Entity +@Table(name = "jhi_authority") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +public class Authority implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull + @Size(max = 50) + @Id + @Column(length = 50) + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Authority)) { + return false; + } + return Objects.equals(name, ((Authority) o).name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + + // prettier-ignore + @Override + public String toString() { + return "Authority{" + + "name='" + name + '\'' + + "}"; + } +} diff --git a/src/main/java/com/adyen/demo/store/domain/CustomerDetails.java b/src/main/java/com/adyen/demo/store/domain/CustomerDetails.java new file mode 100644 index 0000000..4b0d172 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/domain/CustomerDetails.java @@ -0,0 +1,229 @@ +package com.adyen.demo.store.domain; + +import com.adyen.demo.store.domain.enumeration.Gender; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; +import javax.persistence.*; +import javax.validation.constraints.*; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * A CustomerDetails. + */ +@Entity +@Table(name = "customer_details") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class CustomerDetails implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "gender", nullable = false) + private Gender gender; + + @NotNull + @Column(name = "phone", nullable = false) + private String phone; + + @NotNull + @Column(name = "address_line_1", nullable = false) + private String addressLine1; + + @Column(name = "address_line_2") + private String addressLine2; + + @NotNull + @Column(name = "city", nullable = false) + private String city; + + @NotNull + @Column(name = "country", nullable = false) + private String country; + + @OneToOne(optional = false) + @NotNull + @JoinColumn(unique = true) + private User user; + + @OneToMany(mappedBy = "customerDetails") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "orders", "customerDetails" }, allowSetters = true) + private Set carts = new HashSet<>(); + + // jhipster-needle-entity-add-field - JHipster will add fields here + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public CustomerDetails id(Long id) { + this.id = id; + return this; + } + + public Gender getGender() { + return this.gender; + } + + public CustomerDetails gender(Gender gender) { + this.gender = gender; + return this; + } + + public void setGender(Gender gender) { + this.gender = gender; + } + + public String getPhone() { + return this.phone; + } + + public CustomerDetails phone(String phone) { + this.phone = phone; + return this; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getAddressLine1() { + return this.addressLine1; + } + + public CustomerDetails addressLine1(String addressLine1) { + this.addressLine1 = addressLine1; + return this; + } + + public void setAddressLine1(String addressLine1) { + this.addressLine1 = addressLine1; + } + + public String getAddressLine2() { + return this.addressLine2; + } + + public CustomerDetails addressLine2(String addressLine2) { + this.addressLine2 = addressLine2; + return this; + } + + public void setAddressLine2(String addressLine2) { + this.addressLine2 = addressLine2; + } + + public String getCity() { + return this.city; + } + + public CustomerDetails city(String city) { + this.city = city; + return this; + } + + public void setCity(String city) { + this.city = city; + } + + public String getCountry() { + return this.country; + } + + public CustomerDetails country(String country) { + this.country = country; + return this; + } + + public void setCountry(String country) { + this.country = country; + } + + public User getUser() { + return this.user; + } + + public CustomerDetails user(User user) { + this.setUser(user); + return this; + } + + public void setUser(User user) { + this.user = user; + } + + public Set getCarts() { + return this.carts; + } + + public CustomerDetails carts(Set shoppingCarts) { + this.setCarts(shoppingCarts); + return this; + } + + public CustomerDetails addCart(ShoppingCart shoppingCart) { + this.carts.add(shoppingCart); + shoppingCart.setCustomerDetails(this); + return this; + } + + public CustomerDetails removeCart(ShoppingCart shoppingCart) { + this.carts.remove(shoppingCart); + shoppingCart.setCustomerDetails(null); + return this; + } + + public void setCarts(Set shoppingCarts) { + if (this.carts != null) { + this.carts.forEach(i -> i.setCustomerDetails(null)); + } + if (shoppingCarts != null) { + shoppingCarts.forEach(i -> i.setCustomerDetails(this)); + } + this.carts = shoppingCarts; + } + + // jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CustomerDetails)) { + return false; + } + return id != null && id.equals(((CustomerDetails) o).id); + } + + @Override + public int hashCode() { + // see https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/ + return getClass().hashCode(); + } + + // prettier-ignore + @Override + public String toString() { + return "CustomerDetails{" + + "id=" + getId() + + ", gender='" + getGender() + "'" + + ", phone='" + getPhone() + "'" + + ", addressLine1='" + getAddressLine1() + "'" + + ", addressLine2='" + getAddressLine2() + "'" + + ", city='" + getCity() + "'" + + ", country='" + getCountry() + "'" + + "}"; + } +} diff --git a/src/main/java/com/adyen/demo/store/domain/Product.java b/src/main/java/com/adyen/demo/store/domain/Product.java new file mode 100644 index 0000000..2aead71 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/domain/Product.java @@ -0,0 +1,194 @@ +package com.adyen.demo.store.domain; + +import com.adyen.demo.store.domain.enumeration.Size; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.annotations.ApiModel; +import java.io.Serializable; +import java.math.BigDecimal; +import javax.persistence.*; +import javax.validation.constraints.*; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * Product sold by the Online store + */ +@ApiModel(description = "Product sold by the Online store") +@Entity +@Table(name = "product") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class Product implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "description") + private String description; + + @NotNull + @DecimalMin(value = "0") + @Column(name = "price", precision = 21, scale = 2, nullable = false) + private BigDecimal price; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "item_size", nullable = false) + private Size itemSize; + + @Lob + @Column(name = "image") + private byte[] image; + + @Column(name = "image_content_type") + private String imageContentType; + + @ManyToOne(optional = false) + @NotNull + @JsonIgnoreProperties(value = { "products" }, allowSetters = true) + private ProductCategory productCategory; + + // jhipster-needle-entity-add-field - JHipster will add fields here + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Product id(Long id) { + this.id = id; + return this; + } + + public String getName() { + return this.name; + } + + public Product name(String name) { + this.name = name; + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public Product description(String description) { + this.description = description; + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public BigDecimal getPrice() { + return this.price; + } + + public Product price(BigDecimal price) { + this.price = price; + return this; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + public Size getItemSize() { + return this.itemSize; + } + + public Product itemSize(Size itemSize) { + this.itemSize = itemSize; + return this; + } + + public void setItemSize(Size itemSize) { + this.itemSize = itemSize; + } + + public byte[] getImage() { + return this.image; + } + + public Product image(byte[] image) { + this.image = image; + return this; + } + + public void setImage(byte[] image) { + this.image = image; + } + + public String getImageContentType() { + return this.imageContentType; + } + + public Product imageContentType(String imageContentType) { + this.imageContentType = imageContentType; + return this; + } + + public void setImageContentType(String imageContentType) { + this.imageContentType = imageContentType; + } + + public ProductCategory getProductCategory() { + return this.productCategory; + } + + public Product productCategory(ProductCategory productCategory) { + this.setProductCategory(productCategory); + return this; + } + + public void setProductCategory(ProductCategory productCategory) { + this.productCategory = productCategory; + } + + // jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Product)) { + return false; + } + return id != null && id.equals(((Product) o).id); + } + + @Override + public int hashCode() { + // see https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/ + return getClass().hashCode(); + } + + // prettier-ignore + @Override + public String toString() { + return "Product{" + + "id=" + getId() + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + ", price=" + getPrice() + + ", itemSize='" + getItemSize() + "'" + + ", image='" + getImage() + "'" + + ", imageContentType='" + getImageContentType() + "'" + + "}"; + } +} diff --git a/src/main/java/com/adyen/demo/store/domain/ProductCategory.java b/src/main/java/com/adyen/demo/store/domain/ProductCategory.java new file mode 100644 index 0000000..9a2dd51 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/domain/ProductCategory.java @@ -0,0 +1,137 @@ +package com.adyen.demo.store.domain; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; +import javax.persistence.*; +import javax.validation.constraints.*; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * A ProductCategory. + */ +@Entity +@Table(name = "product_category") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class ProductCategory implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "description") + private String description; + + @OneToMany(mappedBy = "productCategory") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "productCategory" }, allowSetters = true) + private Set products = new HashSet<>(); + + // jhipster-needle-entity-add-field - JHipster will add fields here + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public ProductCategory id(Long id) { + this.id = id; + return this; + } + + public String getName() { + return this.name; + } + + public ProductCategory name(String name) { + this.name = name; + return this; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return this.description; + } + + public ProductCategory description(String description) { + this.description = description; + return this; + } + + public void setDescription(String description) { + this.description = description; + } + + public Set getProducts() { + return this.products; + } + + public ProductCategory products(Set products) { + this.setProducts(products); + return this; + } + + public ProductCategory addProduct(Product product) { + this.products.add(product); + product.setProductCategory(this); + return this; + } + + public ProductCategory removeProduct(Product product) { + this.products.remove(product); + product.setProductCategory(null); + return this; + } + + public void setProducts(Set products) { + if (this.products != null) { + this.products.forEach(i -> i.setProductCategory(null)); + } + if (products != null) { + products.forEach(i -> i.setProductCategory(this)); + } + this.products = products; + } + + // jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ProductCategory)) { + return false; + } + return id != null && id.equals(((ProductCategory) o).id); + } + + @Override + public int hashCode() { + // see https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/ + return getClass().hashCode(); + } + + // prettier-ignore + @Override + public String toString() { + return "ProductCategory{" + + "id=" + getId() + + ", name='" + getName() + "'" + + ", description='" + getDescription() + "'" + + "}"; + } +} diff --git a/src/main/java/com/adyen/demo/store/domain/ProductOrder.java b/src/main/java/com/adyen/demo/store/domain/ProductOrder.java new file mode 100644 index 0000000..b1e6de4 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/domain/ProductOrder.java @@ -0,0 +1,139 @@ +package com.adyen.demo.store.domain; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.io.Serializable; +import java.math.BigDecimal; +import javax.persistence.*; +import javax.validation.constraints.*; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * A ProductOrder. + */ +@Entity +@Table(name = "product_order") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class ProductOrder implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Min(value = 0) + @Column(name = "quantity", nullable = false) + private Integer quantity; + + @NotNull + @DecimalMin(value = "0") + @Column(name = "total_price", precision = 21, scale = 2, nullable = false) + private BigDecimal totalPrice; + + @ManyToOne(optional = false) + @NotNull + @JsonIgnoreProperties(value = { "productCategory" }, allowSetters = true) + private Product product; + + @ManyToOne(optional = false) + @NotNull + @JsonIgnoreProperties(value = { "orders", "customerDetails" }, allowSetters = true) + private ShoppingCart cart; + + // jhipster-needle-entity-add-field - JHipster will add fields here + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public ProductOrder id(Long id) { + this.id = id; + return this; + } + + public Integer getQuantity() { + return this.quantity; + } + + public ProductOrder quantity(Integer quantity) { + this.quantity = quantity; + return this; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + public BigDecimal getTotalPrice() { + return this.totalPrice; + } + + public ProductOrder totalPrice(BigDecimal totalPrice) { + this.totalPrice = totalPrice; + return this; + } + + public void setTotalPrice(BigDecimal totalPrice) { + this.totalPrice = totalPrice; + } + + public Product getProduct() { + return this.product; + } + + public ProductOrder product(Product product) { + this.setProduct(product); + return this; + } + + public void setProduct(Product product) { + this.product = product; + } + + public ShoppingCart getCart() { + return this.cart; + } + + public ProductOrder cart(ShoppingCart shoppingCart) { + this.setCart(shoppingCart); + return this; + } + + public void setCart(ShoppingCart shoppingCart) { + this.cart = shoppingCart; + } + + // jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ProductOrder)) { + return false; + } + return id != null && id.equals(((ProductOrder) o).id); + } + + @Override + public int hashCode() { + // see https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/ + return getClass().hashCode(); + } + + // prettier-ignore + @Override + public String toString() { + return "ProductOrder{" + + "id=" + getId() + + ", quantity=" + getQuantity() + + ", totalPrice=" + getTotalPrice() + + "}"; + } +} diff --git a/src/main/java/com/adyen/demo/store/domain/ShoppingCart.java b/src/main/java/com/adyen/demo/store/domain/ShoppingCart.java new file mode 100644 index 0000000..bdc3c33 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/domain/ShoppingCart.java @@ -0,0 +1,233 @@ +package com.adyen.demo.store.domain; + +import com.adyen.demo.store.domain.enumeration.OrderStatus; +import com.adyen.demo.store.domain.enumeration.PaymentMethod; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; +import javax.persistence.*; +import javax.validation.constraints.*; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * A ShoppingCart. + */ +@Entity +@Table(name = "shopping_cart") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +public class ShoppingCart implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Column(name = "placed_date", nullable = false) + private Instant placedDate; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private OrderStatus status; + + @NotNull + @DecimalMin(value = "0") + @Column(name = "total_price", precision = 21, scale = 2, nullable = false) + private BigDecimal totalPrice; + + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "payment_method", nullable = false) + private PaymentMethod paymentMethod; + + @Column(name = "payment_reference") + private String paymentReference; + + @Column(name = "payment_modification_reference") + private String paymentModificationReference; + + @OneToMany(mappedBy = "cart") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @JsonIgnoreProperties(value = { "product", "cart" }, allowSetters = true) + private Set orders = new HashSet<>(); + + @ManyToOne(optional = false) + @NotNull + @JsonIgnoreProperties(value = { "user", "carts" }, allowSetters = true) + private CustomerDetails customerDetails; + + // jhipster-needle-entity-add-field - JHipster will add fields here + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public ShoppingCart id(Long id) { + this.id = id; + return this; + } + + public Instant getPlacedDate() { + return this.placedDate; + } + + public ShoppingCart placedDate(Instant placedDate) { + this.placedDate = placedDate; + return this; + } + + public void setPlacedDate(Instant placedDate) { + this.placedDate = placedDate; + } + + public OrderStatus getStatus() { + return this.status; + } + + public ShoppingCart status(OrderStatus status) { + this.status = status; + return this; + } + + public void setStatus(OrderStatus status) { + this.status = status; + } + + public BigDecimal getTotalPrice() { + return this.totalPrice; + } + + public ShoppingCart totalPrice(BigDecimal totalPrice) { + this.totalPrice = totalPrice; + return this; + } + + public void setTotalPrice(BigDecimal totalPrice) { + this.totalPrice = totalPrice; + } + + public PaymentMethod getPaymentMethod() { + return this.paymentMethod; + } + + public ShoppingCart paymentMethod(PaymentMethod paymentMethod) { + this.paymentMethod = paymentMethod; + return this; + } + + public void setPaymentMethod(PaymentMethod paymentMethod) { + this.paymentMethod = paymentMethod; + } + + public String getPaymentReference() { + return this.paymentReference; + } + + public ShoppingCart paymentReference(String paymentReference) { + this.paymentReference = paymentReference; + return this; + } + + public void setPaymentReference(String paymentReference) { + this.paymentReference = paymentReference; + } + + public String getPaymentModificationReference() { + return this.paymentModificationReference; + } + + public ShoppingCart paymentModificationReference(String paymentModificationReference) { + this.paymentModificationReference = paymentModificationReference; + return this; + } + + public void setPaymentModificationReference(String paymentModificationReference) { + this.paymentModificationReference = paymentModificationReference; + } + + public Set getOrders() { + return this.orders; + } + + public ShoppingCart orders(Set productOrders) { + this.setOrders(productOrders); + return this; + } + + public ShoppingCart addOrder(ProductOrder productOrder) { + this.orders.add(productOrder); + productOrder.setCart(this); + return this; + } + + public ShoppingCart removeOrder(ProductOrder productOrder) { + this.orders.remove(productOrder); + productOrder.setCart(null); + return this; + } + + public void setOrders(Set productOrders) { + if (this.orders != null) { + this.orders.forEach(i -> i.setCart(null)); + } + if (productOrders != null) { + productOrders.forEach(i -> i.setCart(this)); + } + this.orders = productOrders; + } + + public CustomerDetails getCustomerDetails() { + return this.customerDetails; + } + + public ShoppingCart customerDetails(CustomerDetails customerDetails) { + this.setCustomerDetails(customerDetails); + return this; + } + + public void setCustomerDetails(CustomerDetails customerDetails) { + this.customerDetails = customerDetails; + } + + // jhipster-needle-entity-add-getters-setters - JHipster will add getters and setters here + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ShoppingCart)) { + return false; + } + return id != null && id.equals(((ShoppingCart) o).id); + } + + @Override + public int hashCode() { + // see https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/ + return getClass().hashCode(); + } + + // prettier-ignore + @Override + public String toString() { + return "ShoppingCart{" + + "id=" + getId() + + ", placedDate='" + getPlacedDate() + "'" + + ", status='" + getStatus() + "'" + + ", totalPrice=" + getTotalPrice() + + ", paymentMethod='" + getPaymentMethod() + "'" + + ", paymentReference='" + getPaymentReference() + "'" + + ", paymentModificationReference='" + getPaymentModificationReference() + "'" + + "}"; + } +} diff --git a/src/main/java/com/adyen/demo/store/domain/User.java b/src/main/java/com/adyen/demo/store/domain/User.java new file mode 100644 index 0000000..2445421 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/domain/User.java @@ -0,0 +1,231 @@ +package com.adyen.demo.store.domain; + +import com.adyen.demo.store.config.Constants; +import com.fasterxml.jackson.annotation.JsonIgnore; +import java.io.Serializable; +import java.time.Instant; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +import javax.persistence.*; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * A user. + */ +@Entity +@Table(name = "jhi_user") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +public class User extends AbstractAuditingEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Pattern(regexp = Constants.LOGIN_REGEX) + @Size(min = 1, max = 50) + @Column(length = 50, unique = true, nullable = false) + private String login; + + @JsonIgnore + @NotNull + @Size(min = 60, max = 60) + @Column(name = "password_hash", length = 60, nullable = false) + private String password; + + @Size(max = 50) + @Column(name = "first_name", length = 50) + private String firstName; + + @Size(max = 50) + @Column(name = "last_name", length = 50) + private String lastName; + + @Email + @Size(min = 5, max = 254) + @Column(length = 254, unique = true) + private String email; + + @NotNull + @Column(nullable = false) + private boolean activated = false; + + @Size(min = 2, max = 10) + @Column(name = "lang_key", length = 10) + private String langKey; + + @Size(max = 256) + @Column(name = "image_url", length = 256) + private String imageUrl; + + @Size(max = 20) + @Column(name = "activation_key", length = 20) + @JsonIgnore + private String activationKey; + + @Size(max = 20) + @Column(name = "reset_key", length = 20) + @JsonIgnore + private String resetKey; + + @Column(name = "reset_date") + private Instant resetDate = null; + + @JsonIgnore + @ManyToMany + @JoinTable( + name = "jhi_user_authority", + joinColumns = { @JoinColumn(name = "user_id", referencedColumnName = "id") }, + inverseJoinColumns = { @JoinColumn(name = "authority_name", referencedColumnName = "name") } + ) + @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) + @BatchSize(size = 20) + private Set authorities = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLogin() { + return login; + } + + // Lowercase the login before saving it in database + public void setLogin(String login) { + this.login = StringUtils.lowerCase(login, Locale.ENGLISH); + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public boolean isActivated() { + return activated; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } + + public String getActivationKey() { + return activationKey; + } + + public void setActivationKey(String activationKey) { + this.activationKey = activationKey; + } + + public String getResetKey() { + return resetKey; + } + + public void setResetKey(String resetKey) { + this.resetKey = resetKey; + } + + public Instant getResetDate() { + return resetDate; + } + + public void setResetDate(Instant resetDate) { + this.resetDate = resetDate; + } + + public String getLangKey() { + return langKey; + } + + public void setLangKey(String langKey) { + this.langKey = langKey; + } + + public Set getAuthorities() { + return authorities; + } + + public void setAuthorities(Set authorities) { + this.authorities = authorities; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof User)) { + return false; + } + return id != null && id.equals(((User) o).id); + } + + @Override + public int hashCode() { + // see https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/ + return getClass().hashCode(); + } + + // prettier-ignore + @Override + public String toString() { + return "User{" + + "login='" + login + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + ", imageUrl='" + imageUrl + '\'' + + ", activated='" + activated + '\'' + + ", langKey='" + langKey + '\'' + + ", activationKey='" + activationKey + '\'' + + "}"; + } +} diff --git a/src/main/java/com/adyen/demo/store/domain/enumeration/Gender.java b/src/main/java/com/adyen/demo/store/domain/enumeration/Gender.java new file mode 100644 index 0000000..33804b5 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/domain/enumeration/Gender.java @@ -0,0 +1,10 @@ +package com.adyen.demo.store.domain.enumeration; + +/** + * The Gender enumeration. + */ +public enum Gender { + MALE, + FEMALE, + OTHER, +} diff --git a/src/main/java/com/adyen/demo/store/domain/enumeration/OrderStatus.java b/src/main/java/com/adyen/demo/store/domain/enumeration/OrderStatus.java new file mode 100644 index 0000000..99a9351 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/domain/enumeration/OrderStatus.java @@ -0,0 +1,14 @@ +package com.adyen.demo.store.domain.enumeration; + +/** + * The OrderStatus enumeration. + */ +public enum OrderStatus { + REFUND_INITIATED, + REFUND_FAILED, + PAID, + PENDING, + OPEN, + CANCELLED, + REFUNDED, +} diff --git a/src/main/java/com/adyen/demo/store/domain/enumeration/PaymentMethod.java b/src/main/java/com/adyen/demo/store/domain/enumeration/PaymentMethod.java new file mode 100644 index 0000000..d6a9695 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/domain/enumeration/PaymentMethod.java @@ -0,0 +1,19 @@ +package com.adyen.demo.store.domain.enumeration; + +/** + * The PaymentMethod enumeration. + */ +public enum PaymentMethod { + CREDIT_CARD("scheme"), + IDEAL("ideal"); + + private final String value; + + PaymentMethod(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/com/adyen/demo/store/domain/enumeration/Size.java b/src/main/java/com/adyen/demo/store/domain/enumeration/Size.java new file mode 100644 index 0000000..3acfd3f --- /dev/null +++ b/src/main/java/com/adyen/demo/store/domain/enumeration/Size.java @@ -0,0 +1,12 @@ +package com.adyen.demo.store.domain.enumeration; + +/** + * The Size enumeration. + */ +public enum Size { + S, + M, + L, + XL, + XXL, +} diff --git a/src/main/java/com/adyen/demo/store/domain/package-info.java b/src/main/java/com/adyen/demo/store/domain/package-info.java new file mode 100644 index 0000000..8c62897 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/domain/package-info.java @@ -0,0 +1,4 @@ +/** + * JPA domain objects. + */ +package com.adyen.demo.store.domain; diff --git a/src/main/java/com/adyen/demo/store/repository/AuthorityRepository.java b/src/main/java/com/adyen/demo/store/repository/AuthorityRepository.java new file mode 100644 index 0000000..cd6b29c --- /dev/null +++ b/src/main/java/com/adyen/demo/store/repository/AuthorityRepository.java @@ -0,0 +1,9 @@ +package com.adyen.demo.store.repository; + +import com.adyen.demo.store.domain.Authority; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * Spring Data JPA repository for the {@link Authority} entity. + */ +public interface AuthorityRepository extends JpaRepository {} diff --git a/src/main/java/com/adyen/demo/store/repository/CustomerDetailsRepository.java b/src/main/java/com/adyen/demo/store/repository/CustomerDetailsRepository.java new file mode 100644 index 0000000..a5e0076 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/repository/CustomerDetailsRepository.java @@ -0,0 +1,12 @@ +package com.adyen.demo.store.repository; + +import com.adyen.demo.store.domain.CustomerDetails; +import org.springframework.data.jpa.repository.*; +import org.springframework.stereotype.Repository; + +/** + * Spring Data SQL repository for the CustomerDetails entity. + */ +@SuppressWarnings("unused") +@Repository +public interface CustomerDetailsRepository extends JpaRepository {} diff --git a/src/main/java/com/adyen/demo/store/repository/ProductCategoryRepository.java b/src/main/java/com/adyen/demo/store/repository/ProductCategoryRepository.java new file mode 100644 index 0000000..cec682d --- /dev/null +++ b/src/main/java/com/adyen/demo/store/repository/ProductCategoryRepository.java @@ -0,0 +1,12 @@ +package com.adyen.demo.store.repository; + +import com.adyen.demo.store.domain.ProductCategory; +import org.springframework.data.jpa.repository.*; +import org.springframework.stereotype.Repository; + +/** + * Spring Data SQL repository for the ProductCategory entity. + */ +@SuppressWarnings("unused") +@Repository +public interface ProductCategoryRepository extends JpaRepository {} diff --git a/src/main/java/com/adyen/demo/store/repository/ProductOrderRepository.java b/src/main/java/com/adyen/demo/store/repository/ProductOrderRepository.java new file mode 100644 index 0000000..50d0f87 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/repository/ProductOrderRepository.java @@ -0,0 +1,12 @@ +package com.adyen.demo.store.repository; + +import com.adyen.demo.store.domain.ProductOrder; +import org.springframework.data.jpa.repository.*; +import org.springframework.stereotype.Repository; + +/** + * Spring Data SQL repository for the ProductOrder entity. + */ +@SuppressWarnings("unused") +@Repository +public interface ProductOrderRepository extends JpaRepository {} diff --git a/src/main/java/com/adyen/demo/store/repository/ProductRepository.java b/src/main/java/com/adyen/demo/store/repository/ProductRepository.java new file mode 100644 index 0000000..adca2b6 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/repository/ProductRepository.java @@ -0,0 +1,12 @@ +package com.adyen.demo.store.repository; + +import com.adyen.demo.store.domain.Product; +import org.springframework.data.jpa.repository.*; +import org.springframework.stereotype.Repository; + +/** + * Spring Data SQL repository for the Product entity. + */ +@SuppressWarnings("unused") +@Repository +public interface ProductRepository extends JpaRepository {} diff --git a/src/main/java/com/adyen/demo/store/repository/ShoppingCartRepository.java b/src/main/java/com/adyen/demo/store/repository/ShoppingCartRepository.java new file mode 100644 index 0000000..ac25155 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/repository/ShoppingCartRepository.java @@ -0,0 +1,12 @@ +package com.adyen.demo.store.repository; + +import com.adyen.demo.store.domain.ShoppingCart; +import org.springframework.data.jpa.repository.*; +import org.springframework.stereotype.Repository; + +/** + * Spring Data SQL repository for the ShoppingCart entity. + */ +@SuppressWarnings("unused") +@Repository +public interface ShoppingCartRepository extends JpaRepository {} diff --git a/src/main/java/com/adyen/demo/store/repository/UserRepository.java b/src/main/java/com/adyen/demo/store/repository/UserRepository.java new file mode 100644 index 0000000..163e4dd --- /dev/null +++ b/src/main/java/com/adyen/demo/store/repository/UserRepository.java @@ -0,0 +1,42 @@ +package com.adyen.demo.store.repository; + +import com.adyen.demo.store.domain.User; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * Spring Data JPA repository for the {@link User} entity. + */ +@Repository +public interface UserRepository extends JpaRepository { + String USERS_BY_LOGIN_CACHE = "usersByLogin"; + + String USERS_BY_EMAIL_CACHE = "usersByEmail"; + + Optional findOneByActivationKey(String activationKey); + + List findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore(Instant dateTime); + + Optional findOneByResetKey(String resetKey); + + Optional findOneByEmailIgnoreCase(String email); + + Optional findOneByLogin(String login); + + @EntityGraph(attributePaths = "authorities") + @Cacheable(cacheNames = USERS_BY_LOGIN_CACHE) + Optional findOneWithAuthoritiesByLogin(String login); + + @EntityGraph(attributePaths = "authorities") + @Cacheable(cacheNames = USERS_BY_EMAIL_CACHE) + Optional findOneWithAuthoritiesByEmailIgnoreCase(String email); + + Page findAllByIdNotNullAndActivatedIsTrue(Pageable pageable); +} diff --git a/src/main/java/com/adyen/demo/store/repository/package-info.java b/src/main/java/com/adyen/demo/store/repository/package-info.java new file mode 100644 index 0000000..f2c389f --- /dev/null +++ b/src/main/java/com/adyen/demo/store/repository/package-info.java @@ -0,0 +1,4 @@ +/** + * Spring Data JPA repositories. + */ +package com.adyen.demo.store.repository; diff --git a/src/main/java/com/adyen/demo/store/security/AuthoritiesConstants.java b/src/main/java/com/adyen/demo/store/security/AuthoritiesConstants.java new file mode 100644 index 0000000..0660942 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/security/AuthoritiesConstants.java @@ -0,0 +1,15 @@ +package com.adyen.demo.store.security; + +/** + * Constants for Spring Security authorities. + */ +public final class AuthoritiesConstants { + + public static final String ADMIN = "ROLE_ADMIN"; + + public static final String USER = "ROLE_USER"; + + public static final String ANONYMOUS = "ROLE_ANONYMOUS"; + + private AuthoritiesConstants() {} +} diff --git a/src/main/java/com/adyen/demo/store/security/DomainUserDetailsService.java b/src/main/java/com/adyen/demo/store/security/DomainUserDetailsService.java new file mode 100644 index 0000000..5ea93b3 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/security/DomainUserDetailsService.java @@ -0,0 +1,62 @@ +package com.adyen.demo.store.security; + +import com.adyen.demo.store.domain.User; +import com.adyen.demo.store.repository.UserRepository; +import java.util.*; +import java.util.stream.Collectors; +import org.hibernate.validator.internal.constraintvalidators.hv.EmailValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * Authenticate a user from the database. + */ +@Component("userDetailsService") +public class DomainUserDetailsService implements UserDetailsService { + + private final Logger log = LoggerFactory.getLogger(DomainUserDetailsService.class); + + private final UserRepository userRepository; + + public DomainUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + @Transactional + public UserDetails loadUserByUsername(final String login) { + log.debug("Authenticating {}", login); + + if (new EmailValidator().isValid(login, null)) { + return userRepository + .findOneWithAuthoritiesByEmailIgnoreCase(login) + .map(user -> createSpringSecurityUser(login, user)) + .orElseThrow(() -> new UsernameNotFoundException("User with email " + login + " was not found in the database")); + } + + String lowercaseLogin = login.toLowerCase(Locale.ENGLISH); + return userRepository + .findOneWithAuthoritiesByLogin(lowercaseLogin) + .map(user -> createSpringSecurityUser(lowercaseLogin, user)) + .orElseThrow(() -> new UsernameNotFoundException("User " + lowercaseLogin + " was not found in the database")); + } + + private org.springframework.security.core.userdetails.User createSpringSecurityUser(String lowercaseLogin, User user) { + if (!user.isActivated()) { + throw new UserNotActivatedException("User " + lowercaseLogin + " was not activated"); + } + List grantedAuthorities = user + .getAuthorities() + .stream() + .map(authority -> new SimpleGrantedAuthority(authority.getName())) + .collect(Collectors.toList()); + return new org.springframework.security.core.userdetails.User(user.getLogin(), user.getPassword(), grantedAuthorities); + } +} diff --git a/src/main/java/com/adyen/demo/store/security/SecurityUtils.java b/src/main/java/com/adyen/demo/store/security/SecurityUtils.java new file mode 100644 index 0000000..d9b2f2f --- /dev/null +++ b/src/main/java/com/adyen/demo/store/security/SecurityUtils.java @@ -0,0 +1,77 @@ +package com.adyen.demo.store.security; + +import java.util.Optional; +import java.util.stream.Stream; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * Utility class for Spring Security. + */ +public final class SecurityUtils { + + private SecurityUtils() {} + + /** + * Get the login of the current user. + * + * @return the login of the current user. + */ + public static Optional getCurrentUserLogin() { + SecurityContext securityContext = SecurityContextHolder.getContext(); + return Optional.ofNullable(extractPrincipal(securityContext.getAuthentication())); + } + + private static String extractPrincipal(Authentication authentication) { + if (authentication == null) { + return null; + } else if (authentication.getPrincipal() instanceof UserDetails) { + UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal(); + return springSecurityUser.getUsername(); + } else if (authentication.getPrincipal() instanceof String) { + return (String) authentication.getPrincipal(); + } + return null; + } + + /** + * Get the JWT of the current user. + * + * @return the JWT of the current user. + */ + public static Optional getCurrentUserJWT() { + SecurityContext securityContext = SecurityContextHolder.getContext(); + return Optional + .ofNullable(securityContext.getAuthentication()) + .filter(authentication -> authentication.getCredentials() instanceof String) + .map(authentication -> (String) authentication.getCredentials()); + } + + /** + * Check if a user is authenticated. + * + * @return true if the user is authenticated, false otherwise. + */ + public static boolean isAuthenticated() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication != null && getAuthorities(authentication).noneMatch(AuthoritiesConstants.ANONYMOUS::equals); + } + + /** + * Checks if the current user has a specific authority. + * + * @param authority the authority to check. + * @return true if the current user has the authority, false otherwise. + */ + public static boolean hasCurrentUserThisAuthority(String authority) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication != null && getAuthorities(authentication).anyMatch(authority::equals); + } + + private static Stream getAuthorities(Authentication authentication) { + return authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority); + } +} diff --git a/src/main/java/com/adyen/demo/store/security/SpringSecurityAuditorAware.java b/src/main/java/com/adyen/demo/store/security/SpringSecurityAuditorAware.java new file mode 100644 index 0000000..73e45f1 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/security/SpringSecurityAuditorAware.java @@ -0,0 +1,18 @@ +package com.adyen.demo.store.security; + +import com.adyen.demo.store.config.Constants; +import java.util.Optional; +import org.springframework.data.domain.AuditorAware; +import org.springframework.stereotype.Component; + +/** + * Implementation of {@link AuditorAware} based on Spring Security. + */ +@Component +public class SpringSecurityAuditorAware implements AuditorAware { + + @Override + public Optional getCurrentAuditor() { + return Optional.of(SecurityUtils.getCurrentUserLogin().orElse(Constants.SYSTEM)); + } +} diff --git a/src/main/java/com/adyen/demo/store/security/UserNotActivatedException.java b/src/main/java/com/adyen/demo/store/security/UserNotActivatedException.java new file mode 100644 index 0000000..db43bb5 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/security/UserNotActivatedException.java @@ -0,0 +1,19 @@ +package com.adyen.demo.store.security; + +import org.springframework.security.core.AuthenticationException; + +/** + * This exception is thrown in case of a not activated user trying to authenticate. + */ +public class UserNotActivatedException extends AuthenticationException { + + private static final long serialVersionUID = 1L; + + public UserNotActivatedException(String message) { + super(message); + } + + public UserNotActivatedException(String message, Throwable t) { + super(message, t); + } +} diff --git a/src/main/java/com/adyen/demo/store/security/jwt/JWTConfigurer.java b/src/main/java/com/adyen/demo/store/security/jwt/JWTConfigurer.java new file mode 100644 index 0000000..da5c042 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/security/jwt/JWTConfigurer.java @@ -0,0 +1,21 @@ +package com.adyen.demo.store.security.jwt; + +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +public class JWTConfigurer extends SecurityConfigurerAdapter { + + private final TokenProvider tokenProvider; + + public JWTConfigurer(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public void configure(HttpSecurity http) { + JWTFilter customFilter = new JWTFilter(tokenProvider); + http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/src/main/java/com/adyen/demo/store/security/jwt/JWTFilter.java b/src/main/java/com/adyen/demo/store/security/jwt/JWTFilter.java new file mode 100644 index 0000000..6532bcf --- /dev/null +++ b/src/main/java/com/adyen/demo/store/security/jwt/JWTFilter.java @@ -0,0 +1,47 @@ +package com.adyen.demo.store.security.jwt; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +/** + * Filters incoming requests and installs a Spring Security principal if a header corresponding to a valid user is + * found. + */ +public class JWTFilter extends GenericFilterBean { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + + private final TokenProvider tokenProvider; + + public JWTFilter(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + String jwt = resolveToken(httpServletRequest); + if (StringUtils.hasText(jwt) && this.tokenProvider.validateToken(jwt)) { + Authentication authentication = this.tokenProvider.getAuthentication(jwt); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(servletRequest, servletResponse); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(AUTHORIZATION_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/com/adyen/demo/store/security/jwt/TokenProvider.java b/src/main/java/com/adyen/demo/store/security/jwt/TokenProvider.java new file mode 100644 index 0000000..1baed4c --- /dev/null +++ b/src/main/java/com/adyen/demo/store/security/jwt/TokenProvider.java @@ -0,0 +1,100 @@ +package com.adyen.demo.store.security.jwt; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.*; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import tech.jhipster.config.JHipsterProperties; + +@Component +public class TokenProvider { + + private final Logger log = LoggerFactory.getLogger(TokenProvider.class); + + private static final String AUTHORITIES_KEY = "auth"; + + private final Key key; + + private final JwtParser jwtParser; + + private final long tokenValidityInMilliseconds; + + private final long tokenValidityInMillisecondsForRememberMe; + + public TokenProvider(JHipsterProperties jHipsterProperties) { + byte[] keyBytes; + String secret = jHipsterProperties.getSecurity().getAuthentication().getJwt().getSecret(); + if (!ObjectUtils.isEmpty(secret)) { + log.warn( + "Warning: the JWT key used is not Base64-encoded. " + + "We recommend using the `jhipster.security.authentication.jwt.base64-secret` key for optimum security." + ); + keyBytes = secret.getBytes(StandardCharsets.UTF_8); + } else { + log.debug("Using a Base64-encoded JWT secret key"); + keyBytes = Decoders.BASE64.decode(jHipsterProperties.getSecurity().getAuthentication().getJwt().getBase64Secret()); + } + key = Keys.hmacShaKeyFor(keyBytes); + jwtParser = Jwts.parserBuilder().setSigningKey(key).build(); + this.tokenValidityInMilliseconds = 1000 * jHipsterProperties.getSecurity().getAuthentication().getJwt().getTokenValidityInSeconds(); + this.tokenValidityInMillisecondsForRememberMe = + 1000 * jHipsterProperties.getSecurity().getAuthentication().getJwt().getTokenValidityInSecondsForRememberMe(); + } + + public String createToken(Authentication authentication, boolean rememberMe) { + String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(",")); + + long now = (new Date()).getTime(); + Date validity; + if (rememberMe) { + validity = new Date(now + this.tokenValidityInMillisecondsForRememberMe); + } else { + validity = new Date(now + this.tokenValidityInMilliseconds); + } + + return Jwts + .builder() + .setSubject(authentication.getName()) + .claim(AUTHORITIES_KEY, authorities) + .signWith(key, SignatureAlgorithm.HS512) + .setExpiration(validity) + .compact(); + } + + public Authentication getAuthentication(String token) { + Claims claims = jwtParser.parseClaimsJws(token).getBody(); + + Collection authorities = Arrays + .stream(claims.get(AUTHORITIES_KEY).toString().split(",")) + .filter(auth -> !auth.trim().isEmpty()) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + User principal = new User(claims.getSubject(), "", authorities); + + return new UsernamePasswordAuthenticationToken(principal, token, authorities); + } + + public boolean validateToken(String authToken) { + try { + jwtParser.parseClaimsJws(authToken); + return true; + } catch (JwtException | IllegalArgumentException e) { + log.info("Invalid JWT token."); + log.trace("Invalid JWT token trace.", e); + } + return false; + } +} diff --git a/src/main/java/com/adyen/demo/store/security/package-info.java b/src/main/java/com/adyen/demo/store/security/package-info.java new file mode 100644 index 0000000..5ae06a8 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/security/package-info.java @@ -0,0 +1,4 @@ +/** + * Spring Security configuration. + */ +package com.adyen.demo.store.security; diff --git a/src/main/java/com/adyen/demo/store/service/CustomerDetailsService.java b/src/main/java/com/adyen/demo/store/service/CustomerDetailsService.java new file mode 100644 index 0000000..efd8a14 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/CustomerDetailsService.java @@ -0,0 +1,110 @@ +package com.adyen.demo.store.service; + +import com.adyen.demo.store.domain.CustomerDetails; +import com.adyen.demo.store.repository.CustomerDetailsRepository; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Service Implementation for managing {@link CustomerDetails}. + */ +@Service +@Transactional +public class CustomerDetailsService { + + private final Logger log = LoggerFactory.getLogger(CustomerDetailsService.class); + + private final CustomerDetailsRepository customerDetailsRepository; + + public CustomerDetailsService(CustomerDetailsRepository customerDetailsRepository) { + this.customerDetailsRepository = customerDetailsRepository; + } + + /** + * Save a customerDetails. + * + * @param customerDetails the entity to save. + * @return the persisted entity. + */ + public CustomerDetails save(CustomerDetails customerDetails) { + log.debug("Request to save CustomerDetails : {}", customerDetails); + return customerDetailsRepository.save(customerDetails); + } + + /** + * Partially update a customerDetails. + * + * @param customerDetails the entity to update partially. + * @return the persisted entity. + */ + public Optional partialUpdate(CustomerDetails customerDetails) { + log.debug("Request to partially update CustomerDetails : {}", customerDetails); + + return customerDetailsRepository + .findById(customerDetails.getId()) + .map( + existingCustomerDetails -> { + if (customerDetails.getGender() != null) { + existingCustomerDetails.setGender(customerDetails.getGender()); + } + if (customerDetails.getPhone() != null) { + existingCustomerDetails.setPhone(customerDetails.getPhone()); + } + if (customerDetails.getAddressLine1() != null) { + existingCustomerDetails.setAddressLine1(customerDetails.getAddressLine1()); + } + if (customerDetails.getAddressLine2() != null) { + existingCustomerDetails.setAddressLine2(customerDetails.getAddressLine2()); + } + if (customerDetails.getCity() != null) { + existingCustomerDetails.setCity(customerDetails.getCity()); + } + if (customerDetails.getCountry() != null) { + existingCustomerDetails.setCountry(customerDetails.getCountry()); + } + + return existingCustomerDetails; + } + ) + .map(customerDetailsRepository::save); + } + + /** + * Get all the customerDetails. + * + * @param pageable the pagination information. + * @return the list of entities. + */ + @Transactional(readOnly = true) + public Page findAll(Pageable pageable) { + log.debug("Request to get all CustomerDetails"); + return customerDetailsRepository.findAll(pageable); + } + + /** + * Get one customerDetails by id. + * + * @param id the id of the entity. + * @return the entity. + */ + @Transactional(readOnly = true) + public Optional findOne(Long id) { + log.debug("Request to get CustomerDetails : {}", id); + return customerDetailsRepository.findById(id); + } + + /** + * Delete the customerDetails by id. + * + * @param id the id of the entity. + */ + public void delete(Long id) { + log.debug("Request to delete CustomerDetails : {}", id); + customerDetailsRepository.deleteById(id); + } +} diff --git a/src/main/java/com/adyen/demo/store/service/EmailAlreadyUsedException.java b/src/main/java/com/adyen/demo/store/service/EmailAlreadyUsedException.java new file mode 100644 index 0000000..edea26c --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/EmailAlreadyUsedException.java @@ -0,0 +1,10 @@ +package com.adyen.demo.store.service; + +public class EmailAlreadyUsedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public EmailAlreadyUsedException() { + super("Email is already in use!"); + } +} diff --git a/src/main/java/com/adyen/demo/store/service/InvalidPasswordException.java b/src/main/java/com/adyen/demo/store/service/InvalidPasswordException.java new file mode 100644 index 0000000..c4facf7 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/InvalidPasswordException.java @@ -0,0 +1,10 @@ +package com.adyen.demo.store.service; + +public class InvalidPasswordException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public InvalidPasswordException() { + super("Incorrect password"); + } +} diff --git a/src/main/java/com/adyen/demo/store/service/MailService.java b/src/main/java/com/adyen/demo/store/service/MailService.java new file mode 100644 index 0000000..ed054db --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/MailService.java @@ -0,0 +1,112 @@ +package com.adyen.demo.store.service; + +import com.adyen.demo.store.domain.User; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.MessageSource; +import org.springframework.mail.MailException; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring5.SpringTemplateEngine; +import tech.jhipster.config.JHipsterProperties; + +/** + * Service for sending emails. + *

+ * We use the {@link Async} annotation to send emails asynchronously. + */ +@Service +public class MailService { + + private final Logger log = LoggerFactory.getLogger(MailService.class); + + private static final String USER = "user"; + + private static final String BASE_URL = "baseUrl"; + + private final JHipsterProperties jHipsterProperties; + + private final JavaMailSender javaMailSender; + + private final MessageSource messageSource; + + private final SpringTemplateEngine templateEngine; + + public MailService( + JHipsterProperties jHipsterProperties, + JavaMailSender javaMailSender, + MessageSource messageSource, + SpringTemplateEngine templateEngine + ) { + this.jHipsterProperties = jHipsterProperties; + this.javaMailSender = javaMailSender; + this.messageSource = messageSource; + this.templateEngine = templateEngine; + } + + @Async + public void sendEmail(String to, String subject, String content, boolean isMultipart, boolean isHtml) { + log.debug( + "Send email[multipart '{}' and html '{}'] to '{}' with subject '{}' and content={}", + isMultipart, + isHtml, + to, + subject, + content + ); + + // Prepare message using a Spring helper + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + try { + MimeMessageHelper message = new MimeMessageHelper(mimeMessage, isMultipart, StandardCharsets.UTF_8.name()); + message.setTo(to); + message.setFrom(jHipsterProperties.getMail().getFrom()); + message.setSubject(subject); + message.setText(content, isHtml); + javaMailSender.send(mimeMessage); + log.debug("Sent email to User '{}'", to); + } catch (MailException | MessagingException e) { + log.warn("Email could not be sent to user '{}'", to, e); + } + } + + @Async + public void sendEmailFromTemplate(User user, String templateName, String titleKey) { + if (user.getEmail() == null) { + log.debug("Email doesn't exist for user '{}'", user.getLogin()); + return; + } + Locale locale = Locale.forLanguageTag(user.getLangKey()); + Context context = new Context(locale); + context.setVariable(USER, user); + context.setVariable(BASE_URL, jHipsterProperties.getMail().getBaseUrl()); + String content = templateEngine.process(templateName, context); + String subject = messageSource.getMessage(titleKey, null, locale); + sendEmail(user.getEmail(), subject, content, false, true); + } + + @Async + public void sendActivationEmail(User user) { + log.debug("Sending activation email to '{}'", user.getEmail()); + sendEmailFromTemplate(user, "mail/activationEmail", "email.activation.title"); + } + + @Async + public void sendCreationEmail(User user) { + log.debug("Sending creation email to '{}'", user.getEmail()); + sendEmailFromTemplate(user, "mail/creationEmail", "email.activation.title"); + } + + @Async + public void sendPasswordResetMail(User user) { + log.debug("Sending password reset email to '{}'", user.getEmail()); + sendEmailFromTemplate(user, "mail/passwordResetEmail", "email.reset.title"); + } +} diff --git a/src/main/java/com/adyen/demo/store/service/ProductCategoryService.java b/src/main/java/com/adyen/demo/store/service/ProductCategoryService.java new file mode 100644 index 0000000..450e25e --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/ProductCategoryService.java @@ -0,0 +1,98 @@ +package com.adyen.demo.store.service; + +import com.adyen.demo.store.domain.ProductCategory; +import com.adyen.demo.store.repository.ProductCategoryRepository; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Service Implementation for managing {@link ProductCategory}. + */ +@Service +@Transactional +public class ProductCategoryService { + + private final Logger log = LoggerFactory.getLogger(ProductCategoryService.class); + + private final ProductCategoryRepository productCategoryRepository; + + public ProductCategoryService(ProductCategoryRepository productCategoryRepository) { + this.productCategoryRepository = productCategoryRepository; + } + + /** + * Save a productCategory. + * + * @param productCategory the entity to save. + * @return the persisted entity. + */ + public ProductCategory save(ProductCategory productCategory) { + log.debug("Request to save ProductCategory : {}", productCategory); + return productCategoryRepository.save(productCategory); + } + + /** + * Partially update a productCategory. + * + * @param productCategory the entity to update partially. + * @return the persisted entity. + */ + public Optional partialUpdate(ProductCategory productCategory) { + log.debug("Request to partially update ProductCategory : {}", productCategory); + + return productCategoryRepository + .findById(productCategory.getId()) + .map( + existingProductCategory -> { + if (productCategory.getName() != null) { + existingProductCategory.setName(productCategory.getName()); + } + if (productCategory.getDescription() != null) { + existingProductCategory.setDescription(productCategory.getDescription()); + } + + return existingProductCategory; + } + ) + .map(productCategoryRepository::save); + } + + /** + * Get all the productCategories. + * + * @param pageable the pagination information. + * @return the list of entities. + */ + @Transactional(readOnly = true) + public Page findAll(Pageable pageable) { + log.debug("Request to get all ProductCategories"); + return productCategoryRepository.findAll(pageable); + } + + /** + * Get one productCategory by id. + * + * @param id the id of the entity. + * @return the entity. + */ + @Transactional(readOnly = true) + public Optional findOne(Long id) { + log.debug("Request to get ProductCategory : {}", id); + return productCategoryRepository.findById(id); + } + + /** + * Delete the productCategory by id. + * + * @param id the id of the entity. + */ + public void delete(Long id) { + log.debug("Request to delete ProductCategory : {}", id); + productCategoryRepository.deleteById(id); + } +} diff --git a/src/main/java/com/adyen/demo/store/service/ProductOrderService.java b/src/main/java/com/adyen/demo/store/service/ProductOrderService.java new file mode 100644 index 0000000..a4da206 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/ProductOrderService.java @@ -0,0 +1,96 @@ +package com.adyen.demo.store.service; + +import com.adyen.demo.store.domain.ProductOrder; +import com.adyen.demo.store.repository.ProductOrderRepository; +import java.util.List; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Service Implementation for managing {@link ProductOrder}. + */ +@Service +@Transactional +public class ProductOrderService { + + private final Logger log = LoggerFactory.getLogger(ProductOrderService.class); + + private final ProductOrderRepository productOrderRepository; + + public ProductOrderService(ProductOrderRepository productOrderRepository) { + this.productOrderRepository = productOrderRepository; + } + + /** + * Save a productOrder. + * + * @param productOrder the entity to save. + * @return the persisted entity. + */ + public ProductOrder save(ProductOrder productOrder) { + log.debug("Request to save ProductOrder : {}", productOrder); + return productOrderRepository.save(productOrder); + } + + /** + * Partially update a productOrder. + * + * @param productOrder the entity to update partially. + * @return the persisted entity. + */ + public Optional partialUpdate(ProductOrder productOrder) { + log.debug("Request to partially update ProductOrder : {}", productOrder); + + return productOrderRepository + .findById(productOrder.getId()) + .map( + existingProductOrder -> { + if (productOrder.getQuantity() != null) { + existingProductOrder.setQuantity(productOrder.getQuantity()); + } + if (productOrder.getTotalPrice() != null) { + existingProductOrder.setTotalPrice(productOrder.getTotalPrice()); + } + + return existingProductOrder; + } + ) + .map(productOrderRepository::save); + } + + /** + * Get all the productOrders. + * + * @return the list of entities. + */ + @Transactional(readOnly = true) + public List findAll() { + log.debug("Request to get all ProductOrders"); + return productOrderRepository.findAll(); + } + + /** + * Get one productOrder by id. + * + * @param id the id of the entity. + * @return the entity. + */ + @Transactional(readOnly = true) + public Optional findOne(Long id) { + log.debug("Request to get ProductOrder : {}", id); + return productOrderRepository.findById(id); + } + + /** + * Delete the productOrder by id. + * + * @param id the id of the entity. + */ + public void delete(Long id) { + log.debug("Request to delete ProductOrder : {}", id); + productOrderRepository.deleteById(id); + } +} diff --git a/src/main/java/com/adyen/demo/store/service/ProductService.java b/src/main/java/com/adyen/demo/store/service/ProductService.java new file mode 100644 index 0000000..e4bbfca --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/ProductService.java @@ -0,0 +1,110 @@ +package com.adyen.demo.store.service; + +import com.adyen.demo.store.domain.Product; +import com.adyen.demo.store.repository.ProductRepository; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Service Implementation for managing {@link Product}. + */ +@Service +@Transactional +public class ProductService { + + private final Logger log = LoggerFactory.getLogger(ProductService.class); + + private final ProductRepository productRepository; + + public ProductService(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + /** + * Save a product. + * + * @param product the entity to save. + * @return the persisted entity. + */ + public Product save(Product product) { + log.debug("Request to save Product : {}", product); + return productRepository.save(product); + } + + /** + * Partially update a product. + * + * @param product the entity to update partially. + * @return the persisted entity. + */ + public Optional partialUpdate(Product product) { + log.debug("Request to partially update Product : {}", product); + + return productRepository + .findById(product.getId()) + .map( + existingProduct -> { + if (product.getName() != null) { + existingProduct.setName(product.getName()); + } + if (product.getDescription() != null) { + existingProduct.setDescription(product.getDescription()); + } + if (product.getPrice() != null) { + existingProduct.setPrice(product.getPrice()); + } + if (product.getItemSize() != null) { + existingProduct.setItemSize(product.getItemSize()); + } + if (product.getImage() != null) { + existingProduct.setImage(product.getImage()); + } + if (product.getImageContentType() != null) { + existingProduct.setImageContentType(product.getImageContentType()); + } + + return existingProduct; + } + ) + .map(productRepository::save); + } + + /** + * Get all the products. + * + * @param pageable the pagination information. + * @return the list of entities. + */ + @Transactional(readOnly = true) + public Page findAll(Pageable pageable) { + log.debug("Request to get all Products"); + return productRepository.findAll(pageable); + } + + /** + * Get one product by id. + * + * @param id the id of the entity. + * @return the entity. + */ + @Transactional(readOnly = true) + public Optional findOne(Long id) { + log.debug("Request to get Product : {}", id); + return productRepository.findById(id); + } + + /** + * Delete the product by id. + * + * @param id the id of the entity. + */ + public void delete(Long id) { + log.debug("Request to delete Product : {}", id); + productRepository.deleteById(id); + } +} diff --git a/src/main/java/com/adyen/demo/store/service/ShoppingCartService.java b/src/main/java/com/adyen/demo/store/service/ShoppingCartService.java new file mode 100644 index 0000000..6677bac --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/ShoppingCartService.java @@ -0,0 +1,108 @@ +package com.adyen.demo.store.service; + +import com.adyen.demo.store.domain.ShoppingCart; +import com.adyen.demo.store.repository.ShoppingCartRepository; +import java.util.List; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * Service Implementation for managing {@link ShoppingCart}. + */ +@Service +@Transactional +public class ShoppingCartService { + + private final Logger log = LoggerFactory.getLogger(ShoppingCartService.class); + + private final ShoppingCartRepository shoppingCartRepository; + + public ShoppingCartService(ShoppingCartRepository shoppingCartRepository) { + this.shoppingCartRepository = shoppingCartRepository; + } + + /** + * Save a shoppingCart. + * + * @param shoppingCart the entity to save. + * @return the persisted entity. + */ + public ShoppingCart save(ShoppingCart shoppingCart) { + log.debug("Request to save ShoppingCart : {}", shoppingCart); + return shoppingCartRepository.save(shoppingCart); + } + + /** + * Partially update a shoppingCart. + * + * @param shoppingCart the entity to update partially. + * @return the persisted entity. + */ + public Optional partialUpdate(ShoppingCart shoppingCart) { + log.debug("Request to partially update ShoppingCart : {}", shoppingCart); + + return shoppingCartRepository + .findById(shoppingCart.getId()) + .map( + existingShoppingCart -> { + if (shoppingCart.getPlacedDate() != null) { + existingShoppingCart.setPlacedDate(shoppingCart.getPlacedDate()); + } + if (shoppingCart.getStatus() != null) { + existingShoppingCart.setStatus(shoppingCart.getStatus()); + } + if (shoppingCart.getTotalPrice() != null) { + existingShoppingCart.setTotalPrice(shoppingCart.getTotalPrice()); + } + if (shoppingCart.getPaymentMethod() != null) { + existingShoppingCart.setPaymentMethod(shoppingCart.getPaymentMethod()); + } + if (shoppingCart.getPaymentReference() != null) { + existingShoppingCart.setPaymentReference(shoppingCart.getPaymentReference()); + } + if (shoppingCart.getPaymentModificationReference() != null) { + existingShoppingCart.setPaymentModificationReference(shoppingCart.getPaymentModificationReference()); + } + + return existingShoppingCart; + } + ) + .map(shoppingCartRepository::save); + } + + /** + * Get all the shoppingCarts. + * + * @return the list of entities. + */ + @Transactional(readOnly = true) + public List findAll() { + log.debug("Request to get all ShoppingCarts"); + return shoppingCartRepository.findAll(); + } + + /** + * Get one shoppingCart by id. + * + * @param id the id of the entity. + * @return the entity. + */ + @Transactional(readOnly = true) + public Optional findOne(Long id) { + log.debug("Request to get ShoppingCart : {}", id); + return shoppingCartRepository.findById(id); + } + + /** + * Delete the shoppingCart by id. + * + * @param id the id of the entity. + */ + public void delete(Long id) { + log.debug("Request to delete ShoppingCart : {}", id); + shoppingCartRepository.deleteById(id); + } +} diff --git a/src/main/java/com/adyen/demo/store/service/UserService.java b/src/main/java/com/adyen/demo/store/service/UserService.java new file mode 100644 index 0000000..1455d8d --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/UserService.java @@ -0,0 +1,345 @@ +package com.adyen.demo.store.service; + +import com.adyen.demo.store.config.Constants; +import com.adyen.demo.store.domain.Authority; +import com.adyen.demo.store.domain.User; +import com.adyen.demo.store.repository.AuthorityRepository; +import com.adyen.demo.store.repository.UserRepository; +import com.adyen.demo.store.security.AuthoritiesConstants; +import com.adyen.demo.store.security.SecurityUtils; +import com.adyen.demo.store.service.dto.AdminUserDTO; +import com.adyen.demo.store.service.dto.UserDTO; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.CacheManager; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import tech.jhipster.security.RandomUtil; + +/** + * Service class for managing users. + */ +@Service +@Transactional +public class UserService { + + private final Logger log = LoggerFactory.getLogger(UserService.class); + + private final UserRepository userRepository; + + private final PasswordEncoder passwordEncoder; + + private final AuthorityRepository authorityRepository; + + private final CacheManager cacheManager; + + public UserService( + UserRepository userRepository, + PasswordEncoder passwordEncoder, + AuthorityRepository authorityRepository, + CacheManager cacheManager + ) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.authorityRepository = authorityRepository; + this.cacheManager = cacheManager; + } + + public Optional activateRegistration(String key) { + log.debug("Activating user for activation key {}", key); + return userRepository + .findOneByActivationKey(key) + .map( + user -> { + // activate given user for the registration key. + user.setActivated(true); + user.setActivationKey(null); + this.clearUserCaches(user); + log.debug("Activated user: {}", user); + return user; + } + ); + } + + public Optional completePasswordReset(String newPassword, String key) { + log.debug("Reset user password for reset key {}", key); + return userRepository + .findOneByResetKey(key) + .filter(user -> user.getResetDate().isAfter(Instant.now().minusSeconds(86400))) + .map( + user -> { + user.setPassword(passwordEncoder.encode(newPassword)); + user.setResetKey(null); + user.setResetDate(null); + this.clearUserCaches(user); + return user; + } + ); + } + + public Optional requestPasswordReset(String mail) { + return userRepository + .findOneByEmailIgnoreCase(mail) + .filter(User::isActivated) + .map( + user -> { + user.setResetKey(RandomUtil.generateResetKey()); + user.setResetDate(Instant.now()); + this.clearUserCaches(user); + return user; + } + ); + } + + public User registerUser(AdminUserDTO userDTO, String password) { + userRepository + .findOneByLogin(userDTO.getLogin().toLowerCase()) + .ifPresent( + existingUser -> { + boolean removed = removeNonActivatedUser(existingUser); + if (!removed) { + throw new UsernameAlreadyUsedException(); + } + } + ); + userRepository + .findOneByEmailIgnoreCase(userDTO.getEmail()) + .ifPresent( + existingUser -> { + boolean removed = removeNonActivatedUser(existingUser); + if (!removed) { + throw new EmailAlreadyUsedException(); + } + } + ); + User newUser = new User(); + String encryptedPassword = passwordEncoder.encode(password); + newUser.setLogin(userDTO.getLogin().toLowerCase()); + // new user gets initially a generated password + newUser.setPassword(encryptedPassword); + newUser.setFirstName(userDTO.getFirstName()); + newUser.setLastName(userDTO.getLastName()); + if (userDTO.getEmail() != null) { + newUser.setEmail(userDTO.getEmail().toLowerCase()); + } + newUser.setImageUrl(userDTO.getImageUrl()); + newUser.setLangKey(userDTO.getLangKey()); + // new user is not active + newUser.setActivated(false); + // new user gets registration key + newUser.setActivationKey(RandomUtil.generateActivationKey()); + Set authorities = new HashSet<>(); + authorityRepository.findById(AuthoritiesConstants.USER).ifPresent(authorities::add); + newUser.setAuthorities(authorities); + userRepository.save(newUser); + this.clearUserCaches(newUser); + log.debug("Created Information for User: {}", newUser); + return newUser; + } + + private boolean removeNonActivatedUser(User existingUser) { + if (existingUser.isActivated()) { + return false; + } + userRepository.delete(existingUser); + userRepository.flush(); + this.clearUserCaches(existingUser); + return true; + } + + public User createUser(AdminUserDTO userDTO) { + User user = new User(); + user.setLogin(userDTO.getLogin().toLowerCase()); + user.setFirstName(userDTO.getFirstName()); + user.setLastName(userDTO.getLastName()); + if (userDTO.getEmail() != null) { + user.setEmail(userDTO.getEmail().toLowerCase()); + } + user.setImageUrl(userDTO.getImageUrl()); + if (userDTO.getLangKey() == null) { + user.setLangKey(Constants.DEFAULT_LANGUAGE); // default language + } else { + user.setLangKey(userDTO.getLangKey()); + } + String encryptedPassword = passwordEncoder.encode(RandomUtil.generatePassword()); + user.setPassword(encryptedPassword); + user.setResetKey(RandomUtil.generateResetKey()); + user.setResetDate(Instant.now()); + user.setActivated(true); + if (userDTO.getAuthorities() != null) { + Set authorities = userDTO + .getAuthorities() + .stream() + .map(authorityRepository::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + user.setAuthorities(authorities); + } + userRepository.save(user); + this.clearUserCaches(user); + log.debug("Created Information for User: {}", user); + return user; + } + + /** + * Update all information for a specific user, and return the modified user. + * + * @param userDTO user to update. + * @return updated user. + */ + public Optional updateUser(AdminUserDTO userDTO) { + return Optional + .of(userRepository.findById(userDTO.getId())) + .filter(Optional::isPresent) + .map(Optional::get) + .map( + user -> { + this.clearUserCaches(user); + user.setLogin(userDTO.getLogin().toLowerCase()); + user.setFirstName(userDTO.getFirstName()); + user.setLastName(userDTO.getLastName()); + if (userDTO.getEmail() != null) { + user.setEmail(userDTO.getEmail().toLowerCase()); + } + user.setImageUrl(userDTO.getImageUrl()); + user.setActivated(userDTO.isActivated()); + user.setLangKey(userDTO.getLangKey()); + Set managedAuthorities = user.getAuthorities(); + managedAuthorities.clear(); + userDTO + .getAuthorities() + .stream() + .map(authorityRepository::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .forEach(managedAuthorities::add); + this.clearUserCaches(user); + log.debug("Changed Information for User: {}", user); + return user; + } + ) + .map(AdminUserDTO::new); + } + + public void deleteUser(String login) { + userRepository + .findOneByLogin(login) + .ifPresent( + user -> { + userRepository.delete(user); + this.clearUserCaches(user); + log.debug("Deleted User: {}", user); + } + ); + } + + /** + * Update basic information (first name, last name, email, language) for the current user. + * + * @param firstName first name of user. + * @param lastName last name of user. + * @param email email id of user. + * @param langKey language key. + * @param imageUrl image URL of user. + */ + public void updateUser(String firstName, String lastName, String email, String langKey, String imageUrl) { + SecurityUtils + .getCurrentUserLogin() + .flatMap(userRepository::findOneByLogin) + .ifPresent( + user -> { + user.setFirstName(firstName); + user.setLastName(lastName); + if (email != null) { + user.setEmail(email.toLowerCase()); + } + user.setLangKey(langKey); + user.setImageUrl(imageUrl); + this.clearUserCaches(user); + log.debug("Changed Information for User: {}", user); + } + ); + } + + @Transactional + public void changePassword(String currentClearTextPassword, String newPassword) { + SecurityUtils + .getCurrentUserLogin() + .flatMap(userRepository::findOneByLogin) + .ifPresent( + user -> { + String currentEncryptedPassword = user.getPassword(); + if (!passwordEncoder.matches(currentClearTextPassword, currentEncryptedPassword)) { + throw new InvalidPasswordException(); + } + String encryptedPassword = passwordEncoder.encode(newPassword); + user.setPassword(encryptedPassword); + this.clearUserCaches(user); + log.debug("Changed password for User: {}", user); + } + ); + } + + @Transactional(readOnly = true) + public Page getAllManagedUsers(Pageable pageable) { + return userRepository.findAll(pageable).map(AdminUserDTO::new); + } + + @Transactional(readOnly = true) + public Page getAllPublicUsers(Pageable pageable) { + return userRepository.findAllByIdNotNullAndActivatedIsTrue(pageable).map(UserDTO::new); + } + + @Transactional(readOnly = true) + public Optional getUserWithAuthoritiesByLogin(String login) { + return userRepository.findOneWithAuthoritiesByLogin(login); + } + + @Transactional(readOnly = true) + public Optional getUserWithAuthorities() { + return SecurityUtils.getCurrentUserLogin().flatMap(userRepository::findOneWithAuthoritiesByLogin); + } + + /** + * Not activated users should be automatically deleted after 3 days. + *

+ * This is scheduled to get fired everyday, at 01:00 (am). + */ + @Scheduled(cron = "0 0 1 * * ?") + public void removeNotActivatedUsers() { + userRepository + .findAllByActivatedIsFalseAndActivationKeyIsNotNullAndCreatedDateBefore(Instant.now().minus(3, ChronoUnit.DAYS)) + .forEach( + user -> { + log.debug("Deleting not activated user {}", user.getLogin()); + userRepository.delete(user); + this.clearUserCaches(user); + } + ); + } + + /** + * Gets a list of all the authorities. + * @return a list of all the authorities. + */ + @Transactional(readOnly = true) + public List getAuthorities() { + return authorityRepository.findAll().stream().map(Authority::getName).collect(Collectors.toList()); + } + + private void clearUserCaches(User user) { + Objects.requireNonNull(cacheManager.getCache(UserRepository.USERS_BY_LOGIN_CACHE)).evict(user.getLogin()); + if (user.getEmail() != null) { + Objects.requireNonNull(cacheManager.getCache(UserRepository.USERS_BY_EMAIL_CACHE)).evict(user.getEmail()); + } + } +} diff --git a/src/main/java/com/adyen/demo/store/service/UsernameAlreadyUsedException.java b/src/main/java/com/adyen/demo/store/service/UsernameAlreadyUsedException.java new file mode 100644 index 0000000..72aa776 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/UsernameAlreadyUsedException.java @@ -0,0 +1,10 @@ +package com.adyen.demo.store.service; + +public class UsernameAlreadyUsedException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public UsernameAlreadyUsedException() { + super("Login name already used!"); + } +} diff --git a/src/main/java/com/adyen/demo/store/service/dto/AdminUserDTO.java b/src/main/java/com/adyen/demo/store/service/dto/AdminUserDTO.java new file mode 100644 index 0000000..5e3483c --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/dto/AdminUserDTO.java @@ -0,0 +1,193 @@ +package com.adyen.demo.store.service.dto; + +import com.adyen.demo.store.config.Constants; +import com.adyen.demo.store.domain.Authority; +import com.adyen.demo.store.domain.User; +import java.time.Instant; +import java.util.Set; +import java.util.stream.Collectors; +import javax.validation.constraints.*; + +/** + * A DTO representing a user, with his authorities. + */ +public class AdminUserDTO { + + private Long id; + + @NotBlank + @Pattern(regexp = Constants.LOGIN_REGEX) + @Size(min = 1, max = 50) + private String login; + + @Size(max = 50) + private String firstName; + + @Size(max = 50) + private String lastName; + + @Email + @Size(min = 5, max = 254) + private String email; + + @Size(max = 256) + private String imageUrl; + + private boolean activated = false; + + @Size(min = 2, max = 10) + private String langKey; + + private String createdBy; + + private Instant createdDate; + + private String lastModifiedBy; + + private Instant lastModifiedDate; + + private Set authorities; + + public AdminUserDTO() { + // Empty constructor needed for Jackson. + } + + public AdminUserDTO(User user) { + this.id = user.getId(); + this.login = user.getLogin(); + this.firstName = user.getFirstName(); + this.lastName = user.getLastName(); + this.email = user.getEmail(); + this.activated = user.isActivated(); + this.imageUrl = user.getImageUrl(); + this.langKey = user.getLangKey(); + this.createdBy = user.getCreatedBy(); + this.createdDate = user.getCreatedDate(); + this.lastModifiedBy = user.getLastModifiedBy(); + this.lastModifiedDate = user.getLastModifiedDate(); + this.authorities = user.getAuthorities().stream().map(Authority::getName).collect(Collectors.toSet()); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public boolean isActivated() { + return activated; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } + + public String getLangKey() { + return langKey; + } + + public void setLangKey(String langKey) { + this.langKey = langKey; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public Instant getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(Instant createdDate) { + this.createdDate = createdDate; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + + public void setLastModifiedBy(String lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + public Instant getLastModifiedDate() { + return lastModifiedDate; + } + + public void setLastModifiedDate(Instant lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + } + + public Set getAuthorities() { + return authorities; + } + + public void setAuthorities(Set authorities) { + this.authorities = authorities; + } + + // prettier-ignore + @Override + public String toString() { + return "AdminUserDTO{" + + "login='" + login + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + ", imageUrl='" + imageUrl + '\'' + + ", activated=" + activated + + ", langKey='" + langKey + '\'' + + ", createdBy=" + createdBy + + ", createdDate=" + createdDate + + ", lastModifiedBy='" + lastModifiedBy + '\'' + + ", lastModifiedDate=" + lastModifiedDate + + ", authorities=" + authorities + + "}"; + } +} diff --git a/src/main/java/com/adyen/demo/store/service/dto/PasswordChangeDTO.java b/src/main/java/com/adyen/demo/store/service/dto/PasswordChangeDTO.java new file mode 100644 index 0000000..928cef0 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/dto/PasswordChangeDTO.java @@ -0,0 +1,35 @@ +package com.adyen.demo.store.service.dto; + +/** + * A DTO representing a password change required data - current and new password. + */ +public class PasswordChangeDTO { + + private String currentPassword; + private String newPassword; + + public PasswordChangeDTO() { + // Empty constructor needed for Jackson. + } + + public PasswordChangeDTO(String currentPassword, String newPassword) { + this.currentPassword = currentPassword; + this.newPassword = newPassword; + } + + public String getCurrentPassword() { + return currentPassword; + } + + public void setCurrentPassword(String currentPassword) { + this.currentPassword = currentPassword; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} diff --git a/src/main/java/com/adyen/demo/store/service/dto/UserDTO.java b/src/main/java/com/adyen/demo/store/service/dto/UserDTO.java new file mode 100644 index 0000000..d01da49 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/dto/UserDTO.java @@ -0,0 +1,48 @@ +package com.adyen.demo.store.service.dto; + +import com.adyen.demo.store.domain.User; + +/** + * A DTO representing a user, with only the public attributes. + */ +public class UserDTO { + + private Long id; + + private String login; + + public UserDTO() { + // Empty constructor needed for Jackson. + } + + public UserDTO(User user) { + this.id = user.getId(); + // Customize it here if you need, or not, firstName/lastName/etc + this.login = user.getLogin(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + // prettier-ignore + @Override + public String toString() { + return "UserDTO{" + + "id='" + id + '\'' + + ", login='" + login + '\'' + + "}"; + } +} diff --git a/src/main/java/com/adyen/demo/store/service/dto/package-info.java b/src/main/java/com/adyen/demo/store/service/dto/package-info.java new file mode 100644 index 0000000..6fde661 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/dto/package-info.java @@ -0,0 +1,4 @@ +/** + * Data Transfer Objects. + */ +package com.adyen.demo.store.service.dto; diff --git a/src/main/java/com/adyen/demo/store/service/mapper/UserMapper.java b/src/main/java/com/adyen/demo/store/service/mapper/UserMapper.java new file mode 100644 index 0000000..1f2b890 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/mapper/UserMapper.java @@ -0,0 +1,149 @@ +package com.adyen.demo.store.service.mapper; + +import com.adyen.demo.store.domain.Authority; +import com.adyen.demo.store.domain.User; +import com.adyen.demo.store.service.dto.AdminUserDTO; +import com.adyen.demo.store.service.dto.UserDTO; +import java.util.*; +import java.util.stream.Collectors; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.springframework.stereotype.Service; + +/** + * Mapper for the entity {@link User} and its DTO called {@link UserDTO}. + * + * Normal mappers are generated using MapStruct, this one is hand-coded as MapStruct + * support is still in beta, and requires a manual step with an IDE. + */ +@Service +public class UserMapper { + + public List usersToUserDTOs(List users) { + return users.stream().filter(Objects::nonNull).map(this::userToUserDTO).collect(Collectors.toList()); + } + + public UserDTO userToUserDTO(User user) { + return new UserDTO(user); + } + + public List usersToAdminUserDTOs(List users) { + return users.stream().filter(Objects::nonNull).map(this::userToAdminUserDTO).collect(Collectors.toList()); + } + + public AdminUserDTO userToAdminUserDTO(User user) { + return new AdminUserDTO(user); + } + + public List userDTOsToUsers(List userDTOs) { + return userDTOs.stream().filter(Objects::nonNull).map(this::userDTOToUser).collect(Collectors.toList()); + } + + public User userDTOToUser(AdminUserDTO userDTO) { + if (userDTO == null) { + return null; + } else { + User user = new User(); + user.setId(userDTO.getId()); + user.setLogin(userDTO.getLogin()); + user.setFirstName(userDTO.getFirstName()); + user.setLastName(userDTO.getLastName()); + user.setEmail(userDTO.getEmail()); + user.setImageUrl(userDTO.getImageUrl()); + user.setActivated(userDTO.isActivated()); + user.setLangKey(userDTO.getLangKey()); + Set authorities = this.authoritiesFromStrings(userDTO.getAuthorities()); + user.setAuthorities(authorities); + return user; + } + } + + private Set authoritiesFromStrings(Set authoritiesAsString) { + Set authorities = new HashSet<>(); + + if (authoritiesAsString != null) { + authorities = + authoritiesAsString + .stream() + .map( + string -> { + Authority auth = new Authority(); + auth.setName(string); + return auth; + } + ) + .collect(Collectors.toSet()); + } + + return authorities; + } + + public User userFromId(Long id) { + if (id == null) { + return null; + } + User user = new User(); + user.setId(id); + return user; + } + + @Named("id") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + public UserDTO toDtoId(User user) { + if (user == null) { + return null; + } + UserDTO userDto = new UserDTO(); + userDto.setId(user.getId()); + return userDto; + } + + @Named("idSet") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + public Set toDtoIdSet(Set users) { + if (users == null) { + return null; + } + + Set userSet = new HashSet<>(); + for (User userEntity : users) { + userSet.add(this.toDtoId(userEntity)); + } + + return userSet; + } + + @Named("login") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "login", source = "login") + public UserDTO toDtoLogin(User user) { + if (user == null) { + return null; + } + UserDTO userDto = new UserDTO(); + userDto.setId(user.getId()); + userDto.setLogin(user.getLogin()); + return userDto; + } + + @Named("loginSet") + @BeanMapping(ignoreByDefault = true) + @Mapping(target = "id", source = "id") + @Mapping(target = "login", source = "login") + public Set toDtoLoginSet(Set users) { + if (users == null) { + return null; + } + + Set userSet = new HashSet<>(); + for (User userEntity : users) { + userSet.add(this.toDtoLogin(userEntity)); + } + + return userSet; + } +} diff --git a/src/main/java/com/adyen/demo/store/service/mapper/package-info.java b/src/main/java/com/adyen/demo/store/service/mapper/package-info.java new file mode 100644 index 0000000..1f25e2f --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/mapper/package-info.java @@ -0,0 +1,4 @@ +/** + * MapStruct mappers for mapping domain objects and Data Transfer Objects. + */ +package com.adyen.demo.store.service.mapper; diff --git a/src/main/java/com/adyen/demo/store/service/package-info.java b/src/main/java/com/adyen/demo/store/service/package-info.java new file mode 100644 index 0000000..c9527ac --- /dev/null +++ b/src/main/java/com/adyen/demo/store/service/package-info.java @@ -0,0 +1,4 @@ +/** + * Service layer beans. + */ +package com.adyen.demo.store.service; diff --git a/src/main/java/com/adyen/demo/store/web/rest/AccountResource.java b/src/main/java/com/adyen/demo/store/web/rest/AccountResource.java new file mode 100644 index 0000000..a9cbb0a --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/AccountResource.java @@ -0,0 +1,195 @@ +package com.adyen.demo.store.web.rest; + +import com.adyen.demo.store.domain.User; +import com.adyen.demo.store.repository.UserRepository; +import com.adyen.demo.store.security.SecurityUtils; +import com.adyen.demo.store.service.MailService; +import com.adyen.demo.store.service.UserService; +import com.adyen.demo.store.service.dto.AdminUserDTO; +import com.adyen.demo.store.service.dto.PasswordChangeDTO; +import com.adyen.demo.store.service.dto.UserDTO; +import com.adyen.demo.store.web.rest.errors.*; +import com.adyen.demo.store.web.rest.vm.KeyAndPasswordVM; +import com.adyen.demo.store.web.rest.vm.ManagedUserVM; +import java.util.*; +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +/** + * REST controller for managing the current user's account. + */ +@RestController +@RequestMapping("/api") +public class AccountResource { + + private static class AccountResourceException extends RuntimeException { + + private AccountResourceException(String message) { + super(message); + } + } + + private final Logger log = LoggerFactory.getLogger(AccountResource.class); + + private final UserRepository userRepository; + + private final UserService userService; + + private final MailService mailService; + + public AccountResource(UserRepository userRepository, UserService userService, MailService mailService) { + this.userRepository = userRepository; + this.userService = userService; + this.mailService = mailService; + } + + /** + * {@code POST /register} : register the user. + * + * @param managedUserVM the managed user View Model. + * @throws InvalidPasswordException {@code 400 (Bad Request)} if the password is incorrect. + * @throws EmailAlreadyUsedException {@code 400 (Bad Request)} if the email is already used. + * @throws LoginAlreadyUsedException {@code 400 (Bad Request)} if the login is already used. + */ + @PostMapping("/register") + @ResponseStatus(HttpStatus.CREATED) + public void registerAccount(@Valid @RequestBody ManagedUserVM managedUserVM) { + if (isPasswordLengthInvalid(managedUserVM.getPassword())) { + throw new InvalidPasswordException(); + } + User user = userService.registerUser(managedUserVM, managedUserVM.getPassword()); + mailService.sendActivationEmail(user); + } + + /** + * {@code GET /activate} : activate the registered user. + * + * @param key the activation key. + * @throws RuntimeException {@code 500 (Internal Server Error)} if the user couldn't be activated. + */ + @GetMapping("/activate") + public void activateAccount(@RequestParam(value = "key") String key) { + Optional user = userService.activateRegistration(key); + if (!user.isPresent()) { + throw new AccountResourceException("No user was found for this activation key"); + } + } + + /** + * {@code GET /authenticate} : check if the user is authenticated, and return its login. + * + * @param request the HTTP request. + * @return the login if the user is authenticated. + */ + @GetMapping("/authenticate") + public String isAuthenticated(HttpServletRequest request) { + log.debug("REST request to check if the current user is authenticated"); + return request.getRemoteUser(); + } + + /** + * {@code GET /account} : get the current user. + * + * @return the current user. + * @throws RuntimeException {@code 500 (Internal Server Error)} if the user couldn't be returned. + */ + @GetMapping("/account") + public AdminUserDTO getAccount() { + return userService + .getUserWithAuthorities() + .map(AdminUserDTO::new) + .orElseThrow(() -> new AccountResourceException("User could not be found")); + } + + /** + * {@code POST /account} : update the current user information. + * + * @param userDTO the current user information. + * @throws EmailAlreadyUsedException {@code 400 (Bad Request)} if the email is already used. + * @throws RuntimeException {@code 500 (Internal Server Error)} if the user login wasn't found. + */ + @PostMapping("/account") + public void saveAccount(@Valid @RequestBody AdminUserDTO userDTO) { + String userLogin = SecurityUtils + .getCurrentUserLogin() + .orElseThrow(() -> new AccountResourceException("Current user login not found")); + Optional existingUser = userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()); + if (existingUser.isPresent() && (!existingUser.get().getLogin().equalsIgnoreCase(userLogin))) { + throw new EmailAlreadyUsedException(); + } + Optional user = userRepository.findOneByLogin(userLogin); + if (!user.isPresent()) { + throw new AccountResourceException("User could not be found"); + } + userService.updateUser( + userDTO.getFirstName(), + userDTO.getLastName(), + userDTO.getEmail(), + userDTO.getLangKey(), + userDTO.getImageUrl() + ); + } + + /** + * {@code POST /account/change-password} : changes the current user's password. + * + * @param passwordChangeDto current and new password. + * @throws InvalidPasswordException {@code 400 (Bad Request)} if the new password is incorrect. + */ + @PostMapping(path = "/account/change-password") + public void changePassword(@RequestBody PasswordChangeDTO passwordChangeDto) { + if (isPasswordLengthInvalid(passwordChangeDto.getNewPassword())) { + throw new InvalidPasswordException(); + } + userService.changePassword(passwordChangeDto.getCurrentPassword(), passwordChangeDto.getNewPassword()); + } + + /** + * {@code POST /account/reset-password/init} : Send an email to reset the password of the user. + * + * @param mail the mail of the user. + */ + @PostMapping(path = "/account/reset-password/init") + public void requestPasswordReset(@RequestBody String mail) { + Optional user = userService.requestPasswordReset(mail); + if (user.isPresent()) { + mailService.sendPasswordResetMail(user.get()); + } else { + // Pretend the request has been successful to prevent checking which emails really exist + // but log that an invalid attempt has been made + log.warn("Password reset requested for non existing mail"); + } + } + + /** + * {@code POST /account/reset-password/finish} : Finish to reset the password of the user. + * + * @param keyAndPassword the generated key and the new password. + * @throws InvalidPasswordException {@code 400 (Bad Request)} if the password is incorrect. + * @throws RuntimeException {@code 500 (Internal Server Error)} if the password could not be reset. + */ + @PostMapping(path = "/account/reset-password/finish") + public void finishPasswordReset(@RequestBody KeyAndPasswordVM keyAndPassword) { + if (isPasswordLengthInvalid(keyAndPassword.getNewPassword())) { + throw new InvalidPasswordException(); + } + Optional user = userService.completePasswordReset(keyAndPassword.getNewPassword(), keyAndPassword.getKey()); + + if (!user.isPresent()) { + throw new AccountResourceException("No user was found for this reset key"); + } + } + + private static boolean isPasswordLengthInvalid(String password) { + return ( + StringUtils.isEmpty(password) || + password.length() < ManagedUserVM.PASSWORD_MIN_LENGTH || + password.length() > ManagedUserVM.PASSWORD_MAX_LENGTH + ); + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/ClientForwardController.java b/src/main/java/com/adyen/demo/store/web/rest/ClientForwardController.java new file mode 100644 index 0000000..2511bf0 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/ClientForwardController.java @@ -0,0 +1,17 @@ +package com.adyen.demo.store.web.rest; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class ClientForwardController { + + /** + * Forwards any unmapped paths (except those containing a period) to the client {@code index.html}. + * @return forward to client {@code index.html}. + */ + @GetMapping(value = "/**/{path:[^\\.]*}") + public String forward() { + return "forward:/"; + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/CustomerDetailsResource.java b/src/main/java/com/adyen/demo/store/web/rest/CustomerDetailsResource.java new file mode 100644 index 0000000..e2806b5 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/CustomerDetailsResource.java @@ -0,0 +1,184 @@ +package com.adyen.demo.store.web.rest; + +import com.adyen.demo.store.domain.CustomerDetails; +import com.adyen.demo.store.repository.CustomerDetailsRepository; +import com.adyen.demo.store.service.CustomerDetailsService; +import com.adyen.demo.store.web.rest.errors.BadRequestAlertException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.PaginationUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing {@link com.adyen.demo.store.domain.CustomerDetails}. + */ +@RestController +@RequestMapping("/api") +public class CustomerDetailsResource { + + private final Logger log = LoggerFactory.getLogger(CustomerDetailsResource.class); + + private static final String ENTITY_NAME = "customerDetails"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final CustomerDetailsService customerDetailsService; + + private final CustomerDetailsRepository customerDetailsRepository; + + public CustomerDetailsResource(CustomerDetailsService customerDetailsService, CustomerDetailsRepository customerDetailsRepository) { + this.customerDetailsService = customerDetailsService; + this.customerDetailsRepository = customerDetailsRepository; + } + + /** + * {@code POST /customer-details} : Create a new customerDetails. + * + * @param customerDetails the customerDetails to create. + * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new customerDetails, or with status {@code 400 (Bad Request)} if the customerDetails has already an ID. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PostMapping("/customer-details") + public ResponseEntity createCustomerDetails(@Valid @RequestBody CustomerDetails customerDetails) + throws URISyntaxException { + log.debug("REST request to save CustomerDetails : {}", customerDetails); + if (customerDetails.getId() != null) { + throw new BadRequestAlertException("A new customerDetails cannot already have an ID", ENTITY_NAME, "idexists"); + } + CustomerDetails result = customerDetailsService.save(customerDetails); + return ResponseEntity + .created(new URI("/api/customer-details/" + result.getId())) + .headers(HeaderUtil.createEntityCreationAlert(applicationName, false, ENTITY_NAME, result.getId().toString())) + .body(result); + } + + /** + * {@code PUT /customer-details/:id} : Updates an existing customerDetails. + * + * @param id the id of the customerDetails to save. + * @param customerDetails the customerDetails to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated customerDetails, + * or with status {@code 400 (Bad Request)} if the customerDetails is not valid, + * or with status {@code 500 (Internal Server Error)} if the customerDetails couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PutMapping("/customer-details/{id}") + public ResponseEntity updateCustomerDetails( + @PathVariable(value = "id", required = false) final Long id, + @Valid @RequestBody CustomerDetails customerDetails + ) throws URISyntaxException { + log.debug("REST request to update CustomerDetails : {}, {}", id, customerDetails); + if (customerDetails.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, customerDetails.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!customerDetailsRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + CustomerDetails result = customerDetailsService.save(customerDetails); + return ResponseEntity + .ok() + .headers(HeaderUtil.createEntityUpdateAlert(applicationName, false, ENTITY_NAME, customerDetails.getId().toString())) + .body(result); + } + + /** + * {@code PATCH /customer-details/:id} : Partial updates given fields of an existing customerDetails, field will ignore if it is null + * + * @param id the id of the customerDetails to save. + * @param customerDetails the customerDetails to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated customerDetails, + * or with status {@code 400 (Bad Request)} if the customerDetails is not valid, + * or with status {@code 404 (Not Found)} if the customerDetails is not found, + * or with status {@code 500 (Internal Server Error)} if the customerDetails couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PatchMapping(value = "/customer-details/{id}", consumes = "application/merge-patch+json") + public ResponseEntity partialUpdateCustomerDetails( + @PathVariable(value = "id", required = false) final Long id, + @NotNull @RequestBody CustomerDetails customerDetails + ) throws URISyntaxException { + log.debug("REST request to partial update CustomerDetails partially : {}, {}", id, customerDetails); + if (customerDetails.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, customerDetails.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!customerDetailsRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + Optional result = customerDetailsService.partialUpdate(customerDetails); + + return ResponseUtil.wrapOrNotFound( + result, + HeaderUtil.createEntityUpdateAlert(applicationName, false, ENTITY_NAME, customerDetails.getId().toString()) + ); + } + + /** + * {@code GET /customer-details} : get all the customerDetails. + * + * @param pageable the pagination information. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of customerDetails in body. + */ + @GetMapping("/customer-details") + public ResponseEntity> getAllCustomerDetails(Pageable pageable) { + log.debug("REST request to get a page of CustomerDetails"); + Page page = customerDetailsService.findAll(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return ResponseEntity.ok().headers(headers).body(page.getContent()); + } + + /** + * {@code GET /customer-details/:id} : get the "id" customerDetails. + * + * @param id the id of the customerDetails to retrieve. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the customerDetails, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/customer-details/{id}") + public ResponseEntity getCustomerDetails(@PathVariable Long id) { + log.debug("REST request to get CustomerDetails : {}", id); + Optional customerDetails = customerDetailsService.findOne(id); + return ResponseUtil.wrapOrNotFound(customerDetails); + } + + /** + * {@code DELETE /customer-details/:id} : delete the "id" customerDetails. + * + * @param id the id of the customerDetails to delete. + * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. + */ + @DeleteMapping("/customer-details/{id}") + public ResponseEntity deleteCustomerDetails(@PathVariable Long id) { + log.debug("REST request to delete CustomerDetails : {}", id); + customerDetailsService.delete(id); + return ResponseEntity + .noContent() + .headers(HeaderUtil.createEntityDeletionAlert(applicationName, false, ENTITY_NAME, id.toString())) + .build(); + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/ProductCategoryResource.java b/src/main/java/com/adyen/demo/store/web/rest/ProductCategoryResource.java new file mode 100644 index 0000000..2bbd48f --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/ProductCategoryResource.java @@ -0,0 +1,184 @@ +package com.adyen.demo.store.web.rest; + +import com.adyen.demo.store.domain.ProductCategory; +import com.adyen.demo.store.repository.ProductCategoryRepository; +import com.adyen.demo.store.service.ProductCategoryService; +import com.adyen.demo.store.web.rest.errors.BadRequestAlertException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.PaginationUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing {@link com.adyen.demo.store.domain.ProductCategory}. + */ +@RestController +@RequestMapping("/api") +public class ProductCategoryResource { + + private final Logger log = LoggerFactory.getLogger(ProductCategoryResource.class); + + private static final String ENTITY_NAME = "productCategory"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final ProductCategoryService productCategoryService; + + private final ProductCategoryRepository productCategoryRepository; + + public ProductCategoryResource(ProductCategoryService productCategoryService, ProductCategoryRepository productCategoryRepository) { + this.productCategoryService = productCategoryService; + this.productCategoryRepository = productCategoryRepository; + } + + /** + * {@code POST /product-categories} : Create a new productCategory. + * + * @param productCategory the productCategory to create. + * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new productCategory, or with status {@code 400 (Bad Request)} if the productCategory has already an ID. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PostMapping("/product-categories") + public ResponseEntity createProductCategory(@Valid @RequestBody ProductCategory productCategory) + throws URISyntaxException { + log.debug("REST request to save ProductCategory : {}", productCategory); + if (productCategory.getId() != null) { + throw new BadRequestAlertException("A new productCategory cannot already have an ID", ENTITY_NAME, "idexists"); + } + ProductCategory result = productCategoryService.save(productCategory); + return ResponseEntity + .created(new URI("/api/product-categories/" + result.getId())) + .headers(HeaderUtil.createEntityCreationAlert(applicationName, false, ENTITY_NAME, result.getId().toString())) + .body(result); + } + + /** + * {@code PUT /product-categories/:id} : Updates an existing productCategory. + * + * @param id the id of the productCategory to save. + * @param productCategory the productCategory to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated productCategory, + * or with status {@code 400 (Bad Request)} if the productCategory is not valid, + * or with status {@code 500 (Internal Server Error)} if the productCategory couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PutMapping("/product-categories/{id}") + public ResponseEntity updateProductCategory( + @PathVariable(value = "id", required = false) final Long id, + @Valid @RequestBody ProductCategory productCategory + ) throws URISyntaxException { + log.debug("REST request to update ProductCategory : {}, {}", id, productCategory); + if (productCategory.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, productCategory.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!productCategoryRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + ProductCategory result = productCategoryService.save(productCategory); + return ResponseEntity + .ok() + .headers(HeaderUtil.createEntityUpdateAlert(applicationName, false, ENTITY_NAME, productCategory.getId().toString())) + .body(result); + } + + /** + * {@code PATCH /product-categories/:id} : Partial updates given fields of an existing productCategory, field will ignore if it is null + * + * @param id the id of the productCategory to save. + * @param productCategory the productCategory to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated productCategory, + * or with status {@code 400 (Bad Request)} if the productCategory is not valid, + * or with status {@code 404 (Not Found)} if the productCategory is not found, + * or with status {@code 500 (Internal Server Error)} if the productCategory couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PatchMapping(value = "/product-categories/{id}", consumes = "application/merge-patch+json") + public ResponseEntity partialUpdateProductCategory( + @PathVariable(value = "id", required = false) final Long id, + @NotNull @RequestBody ProductCategory productCategory + ) throws URISyntaxException { + log.debug("REST request to partial update ProductCategory partially : {}, {}", id, productCategory); + if (productCategory.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, productCategory.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!productCategoryRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + Optional result = productCategoryService.partialUpdate(productCategory); + + return ResponseUtil.wrapOrNotFound( + result, + HeaderUtil.createEntityUpdateAlert(applicationName, false, ENTITY_NAME, productCategory.getId().toString()) + ); + } + + /** + * {@code GET /product-categories} : get all the productCategories. + * + * @param pageable the pagination information. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of productCategories in body. + */ + @GetMapping("/product-categories") + public ResponseEntity> getAllProductCategories(Pageable pageable) { + log.debug("REST request to get a page of ProductCategories"); + Page page = productCategoryService.findAll(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return ResponseEntity.ok().headers(headers).body(page.getContent()); + } + + /** + * {@code GET /product-categories/:id} : get the "id" productCategory. + * + * @param id the id of the productCategory to retrieve. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the productCategory, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/product-categories/{id}") + public ResponseEntity getProductCategory(@PathVariable Long id) { + log.debug("REST request to get ProductCategory : {}", id); + Optional productCategory = productCategoryService.findOne(id); + return ResponseUtil.wrapOrNotFound(productCategory); + } + + /** + * {@code DELETE /product-categories/:id} : delete the "id" productCategory. + * + * @param id the id of the productCategory to delete. + * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. + */ + @DeleteMapping("/product-categories/{id}") + public ResponseEntity deleteProductCategory(@PathVariable Long id) { + log.debug("REST request to delete ProductCategory : {}", id); + productCategoryService.delete(id); + return ResponseEntity + .noContent() + .headers(HeaderUtil.createEntityDeletionAlert(applicationName, false, ENTITY_NAME, id.toString())) + .build(); + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/ProductOrderResource.java b/src/main/java/com/adyen/demo/store/web/rest/ProductOrderResource.java new file mode 100644 index 0000000..5d3b520 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/ProductOrderResource.java @@ -0,0 +1,174 @@ +package com.adyen.demo.store.web.rest; + +import com.adyen.demo.store.domain.ProductOrder; +import com.adyen.demo.store.repository.ProductOrderRepository; +import com.adyen.demo.store.service.ProductOrderService; +import com.adyen.demo.store.web.rest.errors.BadRequestAlertException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing {@link com.adyen.demo.store.domain.ProductOrder}. + */ +@RestController +@RequestMapping("/api") +public class ProductOrderResource { + + private final Logger log = LoggerFactory.getLogger(ProductOrderResource.class); + + private static final String ENTITY_NAME = "productOrder"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final ProductOrderService productOrderService; + + private final ProductOrderRepository productOrderRepository; + + public ProductOrderResource(ProductOrderService productOrderService, ProductOrderRepository productOrderRepository) { + this.productOrderService = productOrderService; + this.productOrderRepository = productOrderRepository; + } + + /** + * {@code POST /product-orders} : Create a new productOrder. + * + * @param productOrder the productOrder to create. + * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new productOrder, or with status {@code 400 (Bad Request)} if the productOrder has already an ID. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PostMapping("/product-orders") + public ResponseEntity createProductOrder(@Valid @RequestBody ProductOrder productOrder) throws URISyntaxException { + log.debug("REST request to save ProductOrder : {}", productOrder); + if (productOrder.getId() != null) { + throw new BadRequestAlertException("A new productOrder cannot already have an ID", ENTITY_NAME, "idexists"); + } + ProductOrder result = productOrderService.save(productOrder); + return ResponseEntity + .created(new URI("/api/product-orders/" + result.getId())) + .headers(HeaderUtil.createEntityCreationAlert(applicationName, false, ENTITY_NAME, result.getId().toString())) + .body(result); + } + + /** + * {@code PUT /product-orders/:id} : Updates an existing productOrder. + * + * @param id the id of the productOrder to save. + * @param productOrder the productOrder to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated productOrder, + * or with status {@code 400 (Bad Request)} if the productOrder is not valid, + * or with status {@code 500 (Internal Server Error)} if the productOrder couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PutMapping("/product-orders/{id}") + public ResponseEntity updateProductOrder( + @PathVariable(value = "id", required = false) final Long id, + @Valid @RequestBody ProductOrder productOrder + ) throws URISyntaxException { + log.debug("REST request to update ProductOrder : {}, {}", id, productOrder); + if (productOrder.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, productOrder.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!productOrderRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + ProductOrder result = productOrderService.save(productOrder); + return ResponseEntity + .ok() + .headers(HeaderUtil.createEntityUpdateAlert(applicationName, false, ENTITY_NAME, productOrder.getId().toString())) + .body(result); + } + + /** + * {@code PATCH /product-orders/:id} : Partial updates given fields of an existing productOrder, field will ignore if it is null + * + * @param id the id of the productOrder to save. + * @param productOrder the productOrder to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated productOrder, + * or with status {@code 400 (Bad Request)} if the productOrder is not valid, + * or with status {@code 404 (Not Found)} if the productOrder is not found, + * or with status {@code 500 (Internal Server Error)} if the productOrder couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PatchMapping(value = "/product-orders/{id}", consumes = "application/merge-patch+json") + public ResponseEntity partialUpdateProductOrder( + @PathVariable(value = "id", required = false) final Long id, + @NotNull @RequestBody ProductOrder productOrder + ) throws URISyntaxException { + log.debug("REST request to partial update ProductOrder partially : {}, {}", id, productOrder); + if (productOrder.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, productOrder.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!productOrderRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + Optional result = productOrderService.partialUpdate(productOrder); + + return ResponseUtil.wrapOrNotFound( + result, + HeaderUtil.createEntityUpdateAlert(applicationName, false, ENTITY_NAME, productOrder.getId().toString()) + ); + } + + /** + * {@code GET /product-orders} : get all the productOrders. + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of productOrders in body. + */ + @GetMapping("/product-orders") + public List getAllProductOrders() { + log.debug("REST request to get all ProductOrders"); + return productOrderService.findAll(); + } + + /** + * {@code GET /product-orders/:id} : get the "id" productOrder. + * + * @param id the id of the productOrder to retrieve. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the productOrder, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/product-orders/{id}") + public ResponseEntity getProductOrder(@PathVariable Long id) { + log.debug("REST request to get ProductOrder : {}", id); + Optional productOrder = productOrderService.findOne(id); + return ResponseUtil.wrapOrNotFound(productOrder); + } + + /** + * {@code DELETE /product-orders/:id} : delete the "id" productOrder. + * + * @param id the id of the productOrder to delete. + * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. + */ + @DeleteMapping("/product-orders/{id}") + public ResponseEntity deleteProductOrder(@PathVariable Long id) { + log.debug("REST request to delete ProductOrder : {}", id); + productOrderService.delete(id); + return ResponseEntity + .noContent() + .headers(HeaderUtil.createEntityDeletionAlert(applicationName, false, ENTITY_NAME, id.toString())) + .build(); + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/ProductResource.java b/src/main/java/com/adyen/demo/store/web/rest/ProductResource.java new file mode 100644 index 0000000..ca70501 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/ProductResource.java @@ -0,0 +1,183 @@ +package com.adyen.demo.store.web.rest; + +import com.adyen.demo.store.domain.Product; +import com.adyen.demo.store.repository.ProductRepository; +import com.adyen.demo.store.service.ProductService; +import com.adyen.demo.store.web.rest.errors.BadRequestAlertException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.PaginationUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing {@link com.adyen.demo.store.domain.Product}. + */ +@RestController +@RequestMapping("/api") +public class ProductResource { + + private final Logger log = LoggerFactory.getLogger(ProductResource.class); + + private static final String ENTITY_NAME = "product"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final ProductService productService; + + private final ProductRepository productRepository; + + public ProductResource(ProductService productService, ProductRepository productRepository) { + this.productService = productService; + this.productRepository = productRepository; + } + + /** + * {@code POST /products} : Create a new product. + * + * @param product the product to create. + * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new product, or with status {@code 400 (Bad Request)} if the product has already an ID. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PostMapping("/products") + public ResponseEntity createProduct(@Valid @RequestBody Product product) throws URISyntaxException { + log.debug("REST request to save Product : {}", product); + if (product.getId() != null) { + throw new BadRequestAlertException("A new product cannot already have an ID", ENTITY_NAME, "idexists"); + } + Product result = productService.save(product); + return ResponseEntity + .created(new URI("/api/products/" + result.getId())) + .headers(HeaderUtil.createEntityCreationAlert(applicationName, false, ENTITY_NAME, result.getId().toString())) + .body(result); + } + + /** + * {@code PUT /products/:id} : Updates an existing product. + * + * @param id the id of the product to save. + * @param product the product to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated product, + * or with status {@code 400 (Bad Request)} if the product is not valid, + * or with status {@code 500 (Internal Server Error)} if the product couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PutMapping("/products/{id}") + public ResponseEntity updateProduct( + @PathVariable(value = "id", required = false) final Long id, + @Valid @RequestBody Product product + ) throws URISyntaxException { + log.debug("REST request to update Product : {}, {}", id, product); + if (product.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, product.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!productRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + Product result = productService.save(product); + return ResponseEntity + .ok() + .headers(HeaderUtil.createEntityUpdateAlert(applicationName, false, ENTITY_NAME, product.getId().toString())) + .body(result); + } + + /** + * {@code PATCH /products/:id} : Partial updates given fields of an existing product, field will ignore if it is null + * + * @param id the id of the product to save. + * @param product the product to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated product, + * or with status {@code 400 (Bad Request)} if the product is not valid, + * or with status {@code 404 (Not Found)} if the product is not found, + * or with status {@code 500 (Internal Server Error)} if the product couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PatchMapping(value = "/products/{id}", consumes = "application/merge-patch+json") + public ResponseEntity partialUpdateProduct( + @PathVariable(value = "id", required = false) final Long id, + @NotNull @RequestBody Product product + ) throws URISyntaxException { + log.debug("REST request to partial update Product partially : {}, {}", id, product); + if (product.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, product.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!productRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + Optional result = productService.partialUpdate(product); + + return ResponseUtil.wrapOrNotFound( + result, + HeaderUtil.createEntityUpdateAlert(applicationName, false, ENTITY_NAME, product.getId().toString()) + ); + } + + /** + * {@code GET /products} : get all the products. + * + * @param pageable the pagination information. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of products in body. + */ + @GetMapping("/products") + public ResponseEntity> getAllProducts(Pageable pageable) { + log.debug("REST request to get a page of Products"); + Page page = productService.findAll(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return ResponseEntity.ok().headers(headers).body(page.getContent()); + } + + /** + * {@code GET /products/:id} : get the "id" product. + * + * @param id the id of the product to retrieve. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the product, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/products/{id}") + public ResponseEntity getProduct(@PathVariable Long id) { + log.debug("REST request to get Product : {}", id); + Optional product = productService.findOne(id); + return ResponseUtil.wrapOrNotFound(product); + } + + /** + * {@code DELETE /products/:id} : delete the "id" product. + * + * @param id the id of the product to delete. + * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. + */ + @DeleteMapping("/products/{id}") + public ResponseEntity deleteProduct(@PathVariable Long id) { + log.debug("REST request to delete Product : {}", id); + productService.delete(id); + return ResponseEntity + .noContent() + .headers(HeaderUtil.createEntityDeletionAlert(applicationName, false, ENTITY_NAME, id.toString())) + .build(); + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/PublicUserResource.java b/src/main/java/com/adyen/demo/store/web/rest/PublicUserResource.java new file mode 100644 index 0000000..beb7835 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/PublicUserResource.java @@ -0,0 +1,65 @@ +package com.adyen.demo.store.web.rest; + +import com.adyen.demo.store.service.UserService; +import com.adyen.demo.store.service.dto.UserDTO; +import java.util.*; +import java.util.Collections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import tech.jhipster.web.util.PaginationUtil; + +@RestController +@RequestMapping("/api") +public class PublicUserResource { + + private static final List ALLOWED_ORDERED_PROPERTIES = Collections.unmodifiableList( + Arrays.asList("id", "login", "firstName", "lastName", "email", "activated", "langKey") + ); + + private final Logger log = LoggerFactory.getLogger(PublicUserResource.class); + + private final UserService userService; + + public PublicUserResource(UserService userService) { + this.userService = userService; + } + + /** + * {@code GET /users} : get all users with only the public informations - calling this are allowed for anyone. + * + * @param pageable the pagination information. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body all users. + */ + @GetMapping("/users") + public ResponseEntity> getAllPublicUsers(Pageable pageable) { + log.debug("REST request to get all public User names"); + if (!onlyContainsAllowedProperties(pageable)) { + return ResponseEntity.badRequest().build(); + } + + final Page page = userService.getAllPublicUsers(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); + } + + private boolean onlyContainsAllowedProperties(Pageable pageable) { + return pageable.getSort().stream().map(Sort.Order::getProperty).allMatch(ALLOWED_ORDERED_PROPERTIES::contains); + } + + /** + * Gets a list of all roles. + * @return a string list of all roles. + */ + @GetMapping("/authorities") + public List getAuthorities() { + return userService.getAuthorities(); + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/ShoppingCartResource.java b/src/main/java/com/adyen/demo/store/web/rest/ShoppingCartResource.java new file mode 100644 index 0000000..80add97 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/ShoppingCartResource.java @@ -0,0 +1,174 @@ +package com.adyen.demo.store.web.rest; + +import com.adyen.demo.store.domain.ShoppingCart; +import com.adyen.demo.store.repository.ShoppingCartRepository; +import com.adyen.demo.store.service.ShoppingCartService; +import com.adyen.demo.store.web.rest.errors.BadRequestAlertException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing {@link com.adyen.demo.store.domain.ShoppingCart}. + */ +@RestController +@RequestMapping("/api") +public class ShoppingCartResource { + + private final Logger log = LoggerFactory.getLogger(ShoppingCartResource.class); + + private static final String ENTITY_NAME = "shoppingCart"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final ShoppingCartService shoppingCartService; + + private final ShoppingCartRepository shoppingCartRepository; + + public ShoppingCartResource(ShoppingCartService shoppingCartService, ShoppingCartRepository shoppingCartRepository) { + this.shoppingCartService = shoppingCartService; + this.shoppingCartRepository = shoppingCartRepository; + } + + /** + * {@code POST /shopping-carts} : Create a new shoppingCart. + * + * @param shoppingCart the shoppingCart to create. + * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new shoppingCart, or with status {@code 400 (Bad Request)} if the shoppingCart has already an ID. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PostMapping("/shopping-carts") + public ResponseEntity createShoppingCart(@Valid @RequestBody ShoppingCart shoppingCart) throws URISyntaxException { + log.debug("REST request to save ShoppingCart : {}", shoppingCart); + if (shoppingCart.getId() != null) { + throw new BadRequestAlertException("A new shoppingCart cannot already have an ID", ENTITY_NAME, "idexists"); + } + ShoppingCart result = shoppingCartService.save(shoppingCart); + return ResponseEntity + .created(new URI("/api/shopping-carts/" + result.getId())) + .headers(HeaderUtil.createEntityCreationAlert(applicationName, false, ENTITY_NAME, result.getId().toString())) + .body(result); + } + + /** + * {@code PUT /shopping-carts/:id} : Updates an existing shoppingCart. + * + * @param id the id of the shoppingCart to save. + * @param shoppingCart the shoppingCart to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated shoppingCart, + * or with status {@code 400 (Bad Request)} if the shoppingCart is not valid, + * or with status {@code 500 (Internal Server Error)} if the shoppingCart couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PutMapping("/shopping-carts/{id}") + public ResponseEntity updateShoppingCart( + @PathVariable(value = "id", required = false) final Long id, + @Valid @RequestBody ShoppingCart shoppingCart + ) throws URISyntaxException { + log.debug("REST request to update ShoppingCart : {}, {}", id, shoppingCart); + if (shoppingCart.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, shoppingCart.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!shoppingCartRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + ShoppingCart result = shoppingCartService.save(shoppingCart); + return ResponseEntity + .ok() + .headers(HeaderUtil.createEntityUpdateAlert(applicationName, false, ENTITY_NAME, shoppingCart.getId().toString())) + .body(result); + } + + /** + * {@code PATCH /shopping-carts/:id} : Partial updates given fields of an existing shoppingCart, field will ignore if it is null + * + * @param id the id of the shoppingCart to save. + * @param shoppingCart the shoppingCart to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated shoppingCart, + * or with status {@code 400 (Bad Request)} if the shoppingCart is not valid, + * or with status {@code 404 (Not Found)} if the shoppingCart is not found, + * or with status {@code 500 (Internal Server Error)} if the shoppingCart couldn't be updated. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PatchMapping(value = "/shopping-carts/{id}", consumes = "application/merge-patch+json") + public ResponseEntity partialUpdateShoppingCart( + @PathVariable(value = "id", required = false) final Long id, + @NotNull @RequestBody ShoppingCart shoppingCart + ) throws URISyntaxException { + log.debug("REST request to partial update ShoppingCart partially : {}, {}", id, shoppingCart); + if (shoppingCart.getId() == null) { + throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull"); + } + if (!Objects.equals(id, shoppingCart.getId())) { + throw new BadRequestAlertException("Invalid ID", ENTITY_NAME, "idinvalid"); + } + + if (!shoppingCartRepository.existsById(id)) { + throw new BadRequestAlertException("Entity not found", ENTITY_NAME, "idnotfound"); + } + + Optional result = shoppingCartService.partialUpdate(shoppingCart); + + return ResponseUtil.wrapOrNotFound( + result, + HeaderUtil.createEntityUpdateAlert(applicationName, false, ENTITY_NAME, shoppingCart.getId().toString()) + ); + } + + /** + * {@code GET /shopping-carts} : get all the shoppingCarts. + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of shoppingCarts in body. + */ + @GetMapping("/shopping-carts") + public List getAllShoppingCarts() { + log.debug("REST request to get all ShoppingCarts"); + return shoppingCartService.findAll(); + } + + /** + * {@code GET /shopping-carts/:id} : get the "id" shoppingCart. + * + * @param id the id of the shoppingCart to retrieve. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the shoppingCart, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/shopping-carts/{id}") + public ResponseEntity getShoppingCart(@PathVariable Long id) { + log.debug("REST request to get ShoppingCart : {}", id); + Optional shoppingCart = shoppingCartService.findOne(id); + return ResponseUtil.wrapOrNotFound(shoppingCart); + } + + /** + * {@code DELETE /shopping-carts/:id} : delete the "id" shoppingCart. + * + * @param id the id of the shoppingCart to delete. + * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. + */ + @DeleteMapping("/shopping-carts/{id}") + public ResponseEntity deleteShoppingCart(@PathVariable Long id) { + log.debug("REST request to delete ShoppingCart : {}", id); + shoppingCartService.delete(id); + return ResponseEntity + .noContent() + .headers(HeaderUtil.createEntityDeletionAlert(applicationName, false, ENTITY_NAME, id.toString())) + .build(); + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/UserJWTController.java b/src/main/java/com/adyen/demo/store/web/rest/UserJWTController.java new file mode 100644 index 0000000..2f63842 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/UserJWTController.java @@ -0,0 +1,68 @@ +package com.adyen.demo.store.web.rest; + +import com.adyen.demo.store.security.jwt.JWTFilter; +import com.adyen.demo.store.security.jwt.TokenProvider; +import com.adyen.demo.store.web.rest.vm.LoginVM; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.Valid; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +/** + * Controller to authenticate users. + */ +@RestController +@RequestMapping("/api") +public class UserJWTController { + + private final TokenProvider tokenProvider; + + private final AuthenticationManagerBuilder authenticationManagerBuilder; + + public UserJWTController(TokenProvider tokenProvider, AuthenticationManagerBuilder authenticationManagerBuilder) { + this.tokenProvider = tokenProvider; + this.authenticationManagerBuilder = authenticationManagerBuilder; + } + + @PostMapping("/authenticate") + public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM) { + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + loginVM.getUsername(), + loginVM.getPassword() + ); + + Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + String jwt = tokenProvider.createToken(authentication, loginVM.isRememberMe()); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(JWTFilter.AUTHORIZATION_HEADER, "Bearer " + jwt); + return new ResponseEntity<>(new JWTToken(jwt), httpHeaders, HttpStatus.OK); + } + + /** + * Object to return as body in JWT Authentication. + */ + static class JWTToken { + + private String idToken; + + JWTToken(String idToken) { + this.idToken = idToken; + } + + @JsonProperty("id_token") + String getIdToken() { + return idToken; + } + + void setIdToken(String idToken) { + this.idToken = idToken; + } + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/UserResource.java b/src/main/java/com/adyen/demo/store/web/rest/UserResource.java new file mode 100644 index 0000000..b1c2510 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/UserResource.java @@ -0,0 +1,200 @@ +package com.adyen.demo.store.web.rest; + +import com.adyen.demo.store.config.Constants; +import com.adyen.demo.store.domain.User; +import com.adyen.demo.store.repository.UserRepository; +import com.adyen.demo.store.security.AuthoritiesConstants; +import com.adyen.demo.store.service.MailService; +import com.adyen.demo.store.service.UserService; +import com.adyen.demo.store.service.dto.AdminUserDTO; +import com.adyen.demo.store.web.rest.errors.BadRequestAlertException; +import com.adyen.demo.store.web.rest.errors.EmailAlreadyUsedException; +import com.adyen.demo.store.web.rest.errors.LoginAlreadyUsedException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; +import java.util.Collections; +import javax.validation.Valid; +import javax.validation.constraints.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.PaginationUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing users. + *

+ * This class accesses the {@link User} entity, and needs to fetch its collection of authorities. + *

+ * For a normal use-case, it would be better to have an eager relationship between User and Authority, + * and send everything to the client side: there would be no View Model and DTO, a lot less code, and an outer-join + * which would be good for performance. + *

+ * We use a View Model and a DTO for 3 reasons: + *

    + *
  • We want to keep a lazy association between the user and the authorities, because people will + * quite often do relationships with the user, and we don't want them to get the authorities all + * the time for nothing (for performance reasons). This is the #1 goal: we should not impact our users' + * application because of this use-case.
  • + *
  • Not having an outer join causes n+1 requests to the database. This is not a real issue as + * we have by default a second-level cache. This means on the first HTTP call we do the n+1 requests, + * but then all authorities come from the cache, so in fact it's much better than doing an outer join + * (which will get lots of data from the database, for each HTTP call).
  • + *
  • As this manages users, for security reasons, we'd rather have a DTO layer.
  • + *
+ *

+ * Another option would be to have a specific JPA entity graph to handle this case. + */ +@RestController +@RequestMapping("/api/admin") +public class UserResource { + + private static final List ALLOWED_ORDERED_PROPERTIES = Collections.unmodifiableList( + Arrays.asList("id", "login", "firstName", "lastName", "email", "activated", "langKey") + ); + + private final Logger log = LoggerFactory.getLogger(UserResource.class); + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final UserService userService; + + private final UserRepository userRepository; + + private final MailService mailService; + + public UserResource(UserService userService, UserRepository userRepository, MailService mailService) { + this.userService = userService; + this.userRepository = userRepository; + this.mailService = mailService; + } + + /** + * {@code POST /admin/users} : Creates a new user. + *

+ * Creates a new user if the login and email are not already used, and sends an + * mail with an activation link. + * The user needs to be activated on creation. + * + * @param userDTO the user to create. + * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new user, or with status {@code 400 (Bad Request)} if the login or email is already in use. + * @throws URISyntaxException if the Location URI syntax is incorrect. + * @throws BadRequestAlertException {@code 400 (Bad Request)} if the login or email is already in use. + */ + @PostMapping("/users") + @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")") + public ResponseEntity createUser(@Valid @RequestBody AdminUserDTO userDTO) throws URISyntaxException { + log.debug("REST request to save User : {}", userDTO); + + if (userDTO.getId() != null) { + throw new BadRequestAlertException("A new user cannot already have an ID", "userManagement", "idexists"); + // Lowercase the user login before comparing with database + } else if (userRepository.findOneByLogin(userDTO.getLogin().toLowerCase()).isPresent()) { + throw new LoginAlreadyUsedException(); + } else if (userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()).isPresent()) { + throw new EmailAlreadyUsedException(); + } else { + User newUser = userService.createUser(userDTO); + mailService.sendCreationEmail(newUser); + return ResponseEntity + .created(new URI("/api/admin/users/" + newUser.getLogin())) + .headers( + HeaderUtil.createAlert(applicationName, "A user is created with identifier " + newUser.getLogin(), newUser.getLogin()) + ) + .body(newUser); + } + } + + /** + * {@code PUT /admin/users} : Updates an existing User. + * + * @param userDTO the user to update. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated user. + * @throws EmailAlreadyUsedException {@code 400 (Bad Request)} if the email is already in use. + * @throws LoginAlreadyUsedException {@code 400 (Bad Request)} if the login is already in use. + */ + @PutMapping("/users") + @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")") + public ResponseEntity updateUser(@Valid @RequestBody AdminUserDTO userDTO) { + log.debug("REST request to update User : {}", userDTO); + Optional existingUser = userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()); + if (existingUser.isPresent() && (!existingUser.get().getId().equals(userDTO.getId()))) { + throw new EmailAlreadyUsedException(); + } + existingUser = userRepository.findOneByLogin(userDTO.getLogin().toLowerCase()); + if (existingUser.isPresent() && (!existingUser.get().getId().equals(userDTO.getId()))) { + throw new LoginAlreadyUsedException(); + } + Optional updatedUser = userService.updateUser(userDTO); + + return ResponseUtil.wrapOrNotFound( + updatedUser, + HeaderUtil.createAlert(applicationName, "A user is updated with identifier " + userDTO.getLogin(), userDTO.getLogin()) + ); + } + + /** + * {@code GET /admin/users} : get all users with all the details - calling this are only allowed for the administrators. + * + * @param pageable the pagination information. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body all users. + */ + @GetMapping("/users") + @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")") + public ResponseEntity> getAllUsers(Pageable pageable) { + log.debug("REST request to get all User for an admin"); + if (!onlyContainsAllowedProperties(pageable)) { + return ResponseEntity.badRequest().build(); + } + + final Page page = userService.getAllManagedUsers(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); + } + + private boolean onlyContainsAllowedProperties(Pageable pageable) { + return pageable.getSort().stream().map(Sort.Order::getProperty).allMatch(ALLOWED_ORDERED_PROPERTIES::contains); + } + + /** + * {@code GET /admin/users/:login} : get the "login" user. + * + * @param login the login of the user to find. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the "login" user, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/users/{login}") + @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")") + public ResponseEntity getUser(@PathVariable @Pattern(regexp = Constants.LOGIN_REGEX) String login) { + log.debug("REST request to get User : {}", login); + return ResponseUtil.wrapOrNotFound(userService.getUserWithAuthoritiesByLogin(login).map(AdminUserDTO::new)); + } + + /** + * {@code DELETE /admin/users/:login} : delete the "login" User. + * + * @param login the login of the user to delete. + * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. + */ + @DeleteMapping("/users/{login}") + @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")") + public ResponseEntity deleteUser(@PathVariable @Pattern(regexp = Constants.LOGIN_REGEX) String login) { + log.debug("REST request to delete User: {}", login); + userService.deleteUser(login); + return ResponseEntity + .noContent() + .headers(HeaderUtil.createAlert(applicationName, "A user is deleted with identifier " + login, login)) + .build(); + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/errors/BadRequestAlertException.java b/src/main/java/com/adyen/demo/store/web/rest/errors/BadRequestAlertException.java new file mode 100644 index 0000000..15bf86c --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/errors/BadRequestAlertException.java @@ -0,0 +1,41 @@ +package com.adyen.demo.store.web.rest.errors; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import org.zalando.problem.AbstractThrowableProblem; +import org.zalando.problem.Status; + +public class BadRequestAlertException extends AbstractThrowableProblem { + + private static final long serialVersionUID = 1L; + + private final String entityName; + + private final String errorKey; + + public BadRequestAlertException(String defaultMessage, String entityName, String errorKey) { + this(ErrorConstants.DEFAULT_TYPE, defaultMessage, entityName, errorKey); + } + + public BadRequestAlertException(URI type, String defaultMessage, String entityName, String errorKey) { + super(type, defaultMessage, Status.BAD_REQUEST, null, null, null, getAlertParameters(entityName, errorKey)); + this.entityName = entityName; + this.errorKey = errorKey; + } + + public String getEntityName() { + return entityName; + } + + public String getErrorKey() { + return errorKey; + } + + private static Map getAlertParameters(String entityName, String errorKey) { + Map parameters = new HashMap<>(); + parameters.put("message", "error." + errorKey); + parameters.put("params", entityName); + return parameters; + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/errors/EmailAlreadyUsedException.java b/src/main/java/com/adyen/demo/store/web/rest/errors/EmailAlreadyUsedException.java new file mode 100644 index 0000000..1613a03 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/errors/EmailAlreadyUsedException.java @@ -0,0 +1,10 @@ +package com.adyen.demo.store.web.rest.errors; + +public class EmailAlreadyUsedException extends BadRequestAlertException { + + private static final long serialVersionUID = 1L; + + public EmailAlreadyUsedException() { + super(ErrorConstants.EMAIL_ALREADY_USED_TYPE, "Email is already in use!", "userManagement", "emailexists"); + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/errors/ErrorConstants.java b/src/main/java/com/adyen/demo/store/web/rest/errors/ErrorConstants.java new file mode 100644 index 0000000..ac50c64 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/errors/ErrorConstants.java @@ -0,0 +1,17 @@ +package com.adyen.demo.store.web.rest.errors; + +import java.net.URI; + +public final class ErrorConstants { + + public static final String ERR_CONCURRENCY_FAILURE = "error.concurrencyFailure"; + public static final String ERR_VALIDATION = "error.validation"; + public static final String PROBLEM_BASE_URL = "https://www.jhipster.tech/problem"; + public static final URI DEFAULT_TYPE = URI.create(PROBLEM_BASE_URL + "/problem-with-message"); + public static final URI CONSTRAINT_VIOLATION_TYPE = URI.create(PROBLEM_BASE_URL + "/constraint-violation"); + public static final URI INVALID_PASSWORD_TYPE = URI.create(PROBLEM_BASE_URL + "/invalid-password"); + public static final URI EMAIL_ALREADY_USED_TYPE = URI.create(PROBLEM_BASE_URL + "/email-already-used"); + public static final URI LOGIN_ALREADY_USED_TYPE = URI.create(PROBLEM_BASE_URL + "/login-already-used"); + + private ErrorConstants() {} +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/errors/ExceptionTranslator.java b/src/main/java/com/adyen/demo/store/web/rest/errors/ExceptionTranslator.java new file mode 100644 index 0000000..1a781bb --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/errors/ExceptionTranslator.java @@ -0,0 +1,223 @@ +package com.adyen.demo.store.web.rest.errors; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.env.Environment; +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.NativeWebRequest; +import org.zalando.problem.DefaultProblem; +import org.zalando.problem.Problem; +import org.zalando.problem.ProblemBuilder; +import org.zalando.problem.Status; +import org.zalando.problem.StatusType; +import org.zalando.problem.spring.web.advice.ProblemHandling; +import org.zalando.problem.spring.web.advice.security.SecurityAdviceTrait; +import org.zalando.problem.violations.ConstraintViolationProblem; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.web.util.HeaderUtil; + +/** + * Controller advice to translate the server side exceptions to client-friendly json structures. + * The error response follows RFC7807 - Problem Details for HTTP APIs (https://tools.ietf.org/html/rfc7807). + */ +@ControllerAdvice +public class ExceptionTranslator implements ProblemHandling, SecurityAdviceTrait { + + private static final String FIELD_ERRORS_KEY = "fieldErrors"; + private static final String MESSAGE_KEY = "message"; + private static final String PATH_KEY = "path"; + private static final String VIOLATIONS_KEY = "violations"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final Environment env; + + public ExceptionTranslator(Environment env) { + this.env = env; + } + + /** + * Post-process the Problem payload to add the message key for the front-end if needed. + */ + @Override + public ResponseEntity process(@Nullable ResponseEntity entity, NativeWebRequest request) { + if (entity == null) { + return null; + } + Problem problem = entity.getBody(); + if (!(problem instanceof ConstraintViolationProblem || problem instanceof DefaultProblem)) { + return entity; + } + + HttpServletRequest nativeRequest = request.getNativeRequest(HttpServletRequest.class); + String requestUri = nativeRequest != null ? nativeRequest.getRequestURI() : StringUtils.EMPTY; + ProblemBuilder builder = Problem + .builder() + .withType(Problem.DEFAULT_TYPE.equals(problem.getType()) ? ErrorConstants.DEFAULT_TYPE : problem.getType()) + .withStatus(problem.getStatus()) + .withTitle(problem.getTitle()) + .with(PATH_KEY, requestUri); + + if (problem instanceof ConstraintViolationProblem) { + builder + .with(VIOLATIONS_KEY, ((ConstraintViolationProblem) problem).getViolations()) + .with(MESSAGE_KEY, ErrorConstants.ERR_VALIDATION); + } else { + builder.withCause(((DefaultProblem) problem).getCause()).withDetail(problem.getDetail()).withInstance(problem.getInstance()); + problem.getParameters().forEach(builder::with); + if (!problem.getParameters().containsKey(MESSAGE_KEY) && problem.getStatus() != null) { + builder.with(MESSAGE_KEY, "error.http." + problem.getStatus().getStatusCode()); + } + } + return new ResponseEntity<>(builder.build(), entity.getHeaders(), entity.getStatusCode()); + } + + @Override + public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, @Nonnull NativeWebRequest request) { + BindingResult result = ex.getBindingResult(); + List fieldErrors = result + .getFieldErrors() + .stream() + .map( + f -> + new FieldErrorVM( + f.getObjectName().replaceFirst("DTO$", ""), + f.getField(), + StringUtils.isNotBlank(f.getDefaultMessage()) ? f.getDefaultMessage() : f.getCode() + ) + ) + .collect(Collectors.toList()); + + Problem problem = Problem + .builder() + .withType(ErrorConstants.CONSTRAINT_VIOLATION_TYPE) + .withTitle("Method argument not valid") + .withStatus(defaultConstraintViolationStatus()) + .with(MESSAGE_KEY, ErrorConstants.ERR_VALIDATION) + .with(FIELD_ERRORS_KEY, fieldErrors) + .build(); + return create(ex, problem, request); + } + + @ExceptionHandler + public ResponseEntity handleEmailAlreadyUsedException( + com.adyen.demo.store.service.EmailAlreadyUsedException ex, + NativeWebRequest request + ) { + EmailAlreadyUsedException problem = new EmailAlreadyUsedException(); + return create( + problem, + request, + HeaderUtil.createFailureAlert(applicationName, false, problem.getEntityName(), problem.getErrorKey(), problem.getMessage()) + ); + } + + @ExceptionHandler + public ResponseEntity handleUsernameAlreadyUsedException( + com.adyen.demo.store.service.UsernameAlreadyUsedException ex, + NativeWebRequest request + ) { + LoginAlreadyUsedException problem = new LoginAlreadyUsedException(); + return create( + problem, + request, + HeaderUtil.createFailureAlert(applicationName, false, problem.getEntityName(), problem.getErrorKey(), problem.getMessage()) + ); + } + + @ExceptionHandler + public ResponseEntity handleInvalidPasswordException( + com.adyen.demo.store.service.InvalidPasswordException ex, + NativeWebRequest request + ) { + return create(new InvalidPasswordException(), request); + } + + @ExceptionHandler + public ResponseEntity handleBadRequestAlertException(BadRequestAlertException ex, NativeWebRequest request) { + return create( + ex, + request, + HeaderUtil.createFailureAlert(applicationName, false, ex.getEntityName(), ex.getErrorKey(), ex.getMessage()) + ); + } + + @ExceptionHandler + public ResponseEntity handleConcurrencyFailure(ConcurrencyFailureException ex, NativeWebRequest request) { + Problem problem = Problem.builder().withStatus(Status.CONFLICT).with(MESSAGE_KEY, ErrorConstants.ERR_CONCURRENCY_FAILURE).build(); + return create(ex, problem, request); + } + + @Override + public ProblemBuilder prepare(final Throwable throwable, final StatusType status, final URI type) { + Collection activeProfiles = Arrays.asList(env.getActiveProfiles()); + + if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_PRODUCTION)) { + if (throwable instanceof HttpMessageConversionException) { + return Problem + .builder() + .withType(type) + .withTitle(status.getReasonPhrase()) + .withStatus(status) + .withDetail("Unable to convert http message") + .withCause( + Optional.ofNullable(throwable.getCause()).filter(cause -> isCausalChainsEnabled()).map(this::toProblem).orElse(null) + ); + } + if (throwable instanceof DataAccessException) { + return Problem + .builder() + .withType(type) + .withTitle(status.getReasonPhrase()) + .withStatus(status) + .withDetail("Failure during data access") + .withCause( + Optional.ofNullable(throwable.getCause()).filter(cause -> isCausalChainsEnabled()).map(this::toProblem).orElse(null) + ); + } + if (containsPackageName(throwable.getMessage())) { + return Problem + .builder() + .withType(type) + .withTitle(status.getReasonPhrase()) + .withStatus(status) + .withDetail("Unexpected runtime exception") + .withCause( + Optional.ofNullable(throwable.getCause()).filter(cause -> isCausalChainsEnabled()).map(this::toProblem).orElse(null) + ); + } + } + + return Problem + .builder() + .withType(type) + .withTitle(status.getReasonPhrase()) + .withStatus(status) + .withDetail(throwable.getMessage()) + .withCause( + Optional.ofNullable(throwable.getCause()).filter(cause -> isCausalChainsEnabled()).map(this::toProblem).orElse(null) + ); + } + + private boolean containsPackageName(String message) { + // This list is for sure not complete + return StringUtils.containsAny(message, "org.", "java.", "net.", "javax.", "com.", "io.", "de.", "com.adyen.demo.store"); + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/errors/FieldErrorVM.java b/src/main/java/com/adyen/demo/store/web/rest/errors/FieldErrorVM.java new file mode 100644 index 0000000..36c64b2 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/errors/FieldErrorVM.java @@ -0,0 +1,32 @@ +package com.adyen.demo.store.web.rest.errors; + +import java.io.Serializable; + +public class FieldErrorVM implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String objectName; + + private final String field; + + private final String message; + + public FieldErrorVM(String dto, String field, String message) { + this.objectName = dto; + this.field = field; + this.message = message; + } + + public String getObjectName() { + return objectName; + } + + public String getField() { + return field; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/errors/InvalidPasswordException.java b/src/main/java/com/adyen/demo/store/web/rest/errors/InvalidPasswordException.java new file mode 100644 index 0000000..494a113 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/errors/InvalidPasswordException.java @@ -0,0 +1,13 @@ +package com.adyen.demo.store.web.rest.errors; + +import org.zalando.problem.AbstractThrowableProblem; +import org.zalando.problem.Status; + +public class InvalidPasswordException extends AbstractThrowableProblem { + + private static final long serialVersionUID = 1L; + + public InvalidPasswordException() { + super(ErrorConstants.INVALID_PASSWORD_TYPE, "Incorrect password", Status.BAD_REQUEST); + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/errors/LoginAlreadyUsedException.java b/src/main/java/com/adyen/demo/store/web/rest/errors/LoginAlreadyUsedException.java new file mode 100644 index 0000000..eae5024 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/errors/LoginAlreadyUsedException.java @@ -0,0 +1,10 @@ +package com.adyen.demo.store.web.rest.errors; + +public class LoginAlreadyUsedException extends BadRequestAlertException { + + private static final long serialVersionUID = 1L; + + public LoginAlreadyUsedException() { + super(ErrorConstants.LOGIN_ALREADY_USED_TYPE, "Login name already used!", "userManagement", "userexists"); + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/errors/package-info.java b/src/main/java/com/adyen/demo/store/web/rest/errors/package-info.java new file mode 100644 index 0000000..358802d --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/errors/package-info.java @@ -0,0 +1,6 @@ +/** + * Specific errors used with Zalando's "problem-spring-web" library. + * + * More information on https://github.com/zalando/problem-spring-web + */ +package com.adyen.demo.store.web.rest.errors; diff --git a/src/main/java/com/adyen/demo/store/web/rest/package-info.java b/src/main/java/com/adyen/demo/store/web/rest/package-info.java new file mode 100644 index 0000000..84a1335 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/package-info.java @@ -0,0 +1,4 @@ +/** + * Spring MVC REST controllers. + */ +package com.adyen.demo.store.web.rest; diff --git a/src/main/java/com/adyen/demo/store/web/rest/vm/KeyAndPasswordVM.java b/src/main/java/com/adyen/demo/store/web/rest/vm/KeyAndPasswordVM.java new file mode 100644 index 0000000..a8e4591 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/vm/KeyAndPasswordVM.java @@ -0,0 +1,27 @@ +package com.adyen.demo.store.web.rest.vm; + +/** + * View Model object for storing the user's key and password. + */ +public class KeyAndPasswordVM { + + private String key; + + private String newPassword; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/vm/LoginVM.java b/src/main/java/com/adyen/demo/store/web/rest/vm/LoginVM.java new file mode 100644 index 0000000..7806d3e --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/vm/LoginVM.java @@ -0,0 +1,53 @@ +package com.adyen.demo.store.web.rest.vm; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * View Model object for storing a user's credentials. + */ +public class LoginVM { + + @NotNull + @Size(min = 1, max = 50) + private String username; + + @NotNull + @Size(min = 4, max = 100) + private String password; + + private boolean rememberMe; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isRememberMe() { + return rememberMe; + } + + public void setRememberMe(boolean rememberMe) { + this.rememberMe = rememberMe; + } + + // prettier-ignore + @Override + public String toString() { + return "LoginVM{" + + "username='" + username + '\'' + + ", rememberMe=" + rememberMe + + '}'; + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/vm/ManagedUserVM.java b/src/main/java/com/adyen/demo/store/web/rest/vm/ManagedUserVM.java new file mode 100644 index 0000000..29ad8c5 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/vm/ManagedUserVM.java @@ -0,0 +1,35 @@ +package com.adyen.demo.store.web.rest.vm; + +import com.adyen.demo.store.service.dto.AdminUserDTO; +import javax.validation.constraints.Size; + +/** + * View Model extending the AdminUserDTO, which is meant to be used in the user management UI. + */ +public class ManagedUserVM extends AdminUserDTO { + + public static final int PASSWORD_MIN_LENGTH = 4; + + public static final int PASSWORD_MAX_LENGTH = 100; + + @Size(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH) + private String password; + + public ManagedUserVM() { + // Empty constructor needed for Jackson. + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + // prettier-ignore + @Override + public String toString() { + return "ManagedUserVM{" + super.toString() + "} "; + } +} diff --git a/src/main/java/com/adyen/demo/store/web/rest/vm/package-info.java b/src/main/java/com/adyen/demo/store/web/rest/vm/package-info.java new file mode 100644 index 0000000..6c6de81 --- /dev/null +++ b/src/main/java/com/adyen/demo/store/web/rest/vm/package-info.java @@ -0,0 +1,4 @@ +/** + * View Models used by Spring MVC REST controllers. + */ +package com.adyen.demo.store.web.rest.vm; diff --git a/src/main/resources/.h2.server.properties b/src/main/resources/.h2.server.properties new file mode 100644 index 0000000..2e70b32 --- /dev/null +++ b/src/main/resources/.h2.server.properties @@ -0,0 +1,5 @@ +#H2 Server Properties +0=JHipster H2 (Disk)|org.h2.Driver|jdbc\:h2\:file\:./build/h2db/db/store|store +webAllowOthers=true +webPort=8092 +webSSL=false diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..c78ac66 --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,10 @@ + + ${AnsiColor.GREEN} ██╗${AnsiColor.CYAN} ██╗ ██╗ ████████╗ ███████╗ ██████╗ ████████╗ ████████╗ ███████╗ + ${AnsiColor.GREEN} ██║${AnsiColor.CYAN} ██║ ██║ ╚══██╔══╝ ██╔═══██╗ ██╔════╝ ╚══██╔══╝ ██╔═════╝ ██╔═══██╗ + ${AnsiColor.GREEN} ██║${AnsiColor.CYAN} ████████║ ██║ ███████╔╝ ╚█████╗ ██║ ██████╗ ███████╔╝ + ${AnsiColor.GREEN}██╗ ██║${AnsiColor.CYAN} ██╔═══██║ ██║ ██╔════╝ ╚═══██╗ ██║ ██╔═══╝ ██╔══██║ + ${AnsiColor.GREEN}╚██████╔╝${AnsiColor.CYAN} ██║ ██║ ████████╗ ██║ ██████╔╝ ██║ ████████╗ ██║ ╚██╗ + ${AnsiColor.GREEN} ╚═════╝ ${AnsiColor.CYAN} ╚═╝ ╚═╝ ╚═══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══════╝ ╚═╝ ╚═╝ + +${AnsiColor.BRIGHT_BLUE}:: JHipster 🤓 :: Running Spring Boot ${spring-boot.version} :: +:: https://www.jhipster.tech ::${AnsiColor.DEFAULT} diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml new file mode 100644 index 0000000..1809664 --- /dev/null +++ b/src/main/resources/config/application-dev.yml @@ -0,0 +1,108 @@ +# =================================================================== +# Spring Boot configuration for the "dev" profile. +# +# This configuration overrides the application.yml file. +# +# More information on profiles: https://www.jhipster.tech/profiles/ +# More information on configuration properties: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +logging: + level: + ROOT: DEBUG + tech.jhipster: DEBUG + org.hibernate.SQL: DEBUG + com.adyen.demo.store: DEBUG + +spring: + devtools: + restart: + enabled: true + additional-exclude: static/**,.h2.server.properties + livereload: + enabled: false # we use Webpack dev server + BrowserSync for livereload + jackson: + serialization: + indent-output: true + datasource: + type: com.zaxxer.hikari.HikariDataSource + url: jdbc:h2:file:./build/h2db/db/store;DB_CLOSE_DELAY=-1 + username: store + password: + hikari: + poolName: Hikari + auto-commit: false + h2: + console: + enabled: false + jpa: + database-platform: tech.jhipster.domain.util.FixedH2Dialect + liquibase: + # Remove 'faker' if you do not want the sample data to be loaded automatically + contexts: dev, faker + mail: + host: localhost + port: 25 + username: + password: + messages: + cache-duration: PT1S # 1 second, see the ISO 8601 standard + thymeleaf: + cache: false + +server: + port: 8080 + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +jhipster: + cache: # Cache configuration + ehcache: # Ehcache configuration + time-to-live-seconds: 3600 # By default objects stay 1 hour in the cache + max-entries: 100 # Number of objects in each cache entry + # CORS is only enabled by default with the "dev" profile + cors: + # Allow Ionic for JHipster by default (* no longer allowed in Spring Boot 2.4+) + allowed-origins: 'http://localhost:8100' + allowed-methods: '*' + allowed-headers: '*' + exposed-headers: 'Authorization,Link,X-Total-Count,X-${jhipster.clientApp.name}-alert,X-${jhipster.clientApp.name}-error,X-${jhipster.clientApp.name}-params' + allow-credentials: true + max-age: 1800 + security: + authentication: + jwt: + # This token must be encoded using Base64 and be at least 256 bits long (you can type `openssl rand -base64 64` on your command line to generate a 512 bits one) + base64-secret: NTMyMGZkYWMzYTVmOWMwMWUwOWNhYjllYzM0MTg2ZDZmNzZmMWZhNjcwYjc2ODA2ZGZjMGJlYTc4YzM3YzczMTFiNjBlYjczMWM4NDM3NTM0N2RkOTNiMWJmOWE4YTUxOTkzMzQ0Zjc2MTNmN2U4NjMyZGMzYjRiNzUwZTI2OTA= + # Token is valid 24 hours + token-validity-in-seconds: 86400 + token-validity-in-seconds-for-remember-me: 2592000 + mail: # specific JHipster mail property, for standard properties see MailProperties + base-url: http://127.0.0.1:8080 + logging: + use-json-format: false # By default, logs are not in Json format + logstash: # Forward logs to logstash over a socket, used by LoggingConfiguration + enabled: false + host: localhost + port: 5000 + queue-size: 512 +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# application: diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml new file mode 100644 index 0000000..17761b7 --- /dev/null +++ b/src/main/resources/config/application-prod.yml @@ -0,0 +1,130 @@ +# =================================================================== +# Spring Boot configuration for the "prod" profile. +# +# This configuration overrides the application.yml file. +# +# More information on profiles: https://www.jhipster.tech/profiles/ +# More information on configuration properties: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +logging: + level: + ROOT: INFO + tech.jhipster: INFO + com.adyen.demo.store: INFO + +management: + metrics: + export: + prometheus: + enabled: false + +spring: + devtools: + restart: + enabled: false + livereload: + enabled: false + datasource: + type: com.zaxxer.hikari.HikariDataSource + url: jdbc:mysql://localhost:3306/store?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=UTC&createDatabaseIfNotExist=true + username: root + password: + hikari: + poolName: Hikari + auto-commit: false + data-source-properties: + cachePrepStmts: true + prepStmtCacheSize: 250 + prepStmtCacheSqlLimit: 2048 + useServerPrepStmts: true + jpa: + # Replace by 'prod, faker' to add the faker context and have sample data loaded in production + liquibase: + contexts: prod + mail: + host: localhost + port: 25 + username: + password: + thymeleaf: + cache: true + +# =================================================================== +# To enable TLS in production, generate a certificate using: +# keytool -genkey -alias store -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 3650 +# +# You can also use Let's Encrypt: +# https://maximilian-boehm.com/hp2121/Create-a-Java-Keystore-JKS-from-Let-s-Encrypt-Certificates.htm +# +# Then, modify the server.ssl properties so your "server" configuration looks like: +# +# server: +# port: 443 +# ssl: +# key-store: classpath:config/tls/keystore.p12 +# key-store-password: password +# key-store-type: PKCS12 +# key-alias: selfsigned +# # The ciphers suite enforce the security by deactivating some old and deprecated SSL cipher, this list was tested against SSL Labs (https://www.ssllabs.com/ssltest/) +# ciphers: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 ,TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 ,TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 ,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,TLS_DHE_RSA_WITH_AES_128_CBC_SHA256,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA256,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_AES_128_GCM_SHA256,TLS_RSA_WITH_AES_256_GCM_SHA384,TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_RSA_WITH_AES_256_CBC_SHA256,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA,TLS_RSA_WITH_CAMELLIA_256_CBC_SHA,TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA,TLS_RSA_WITH_CAMELLIA_128_CBC_SHA +# =================================================================== +server: + port: 8080 + shutdown: graceful # see https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-graceful-shutdown + compression: + enabled: true + mime-types: text/html,text/xml,text/plain,text/css, application/javascript, application/json + min-response-size: 1024 + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +jhipster: + http: + cache: # Used by the CachingHttpHeadersFilter + timeToLiveInDays: 1461 + cache: # Cache configuration + ehcache: # Ehcache configuration + time-to-live-seconds: 3600 # By default objects stay 1 hour in the cache + max-entries: 1000 # Number of objects in each cache entry + security: + authentication: + jwt: + # This token must be encoded using Base64 and be at least 256 bits long (you can type `openssl rand -base64 64` on your command line to generate a 512 bits one) + # As this is the PRODUCTION configuration, you MUST change the default key, and store it securely: + # - In the JHipster Registry (which includes a Spring Cloud Config server) + # - In a separate `application-prod.yml` file, in the same folder as your executable JAR file + # - In the `JHIPSTER_SECURITY_AUTHENTICATION_JWT_BASE64_SECRET` environment variable + base64-secret: NTMyMGZkYWMzYTVmOWMwMWUwOWNhYjllYzM0MTg2ZDZmNzZmMWZhNjcwYjc2ODA2ZGZjMGJlYTc4YzM3YzczMTFiNjBlYjczMWM4NDM3NTM0N2RkOTNiMWJmOWE4YTUxOTkzMzQ0Zjc2MTNmN2U4NjMyZGMzYjRiNzUwZTI2OTA= + # Token is valid 24 hours + token-validity-in-seconds: 86400 + token-validity-in-seconds-for-remember-me: 2592000 + mail: # specific JHipster mail property, for standard properties see MailProperties + base-url: http://my-server-url-to-change # Modify according to your server's URL + logging: + use-json-format: false # By default, logs are not in Json format + logstash: # Forward logs to logstash over a socket, used by LoggingConfiguration + enabled: false + host: localhost + port: 5000 + queue-size: 512 +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# application: diff --git a/src/main/resources/config/application-tls.yml b/src/main/resources/config/application-tls.yml new file mode 100644 index 0000000..27c9cd8 --- /dev/null +++ b/src/main/resources/config/application-tls.yml @@ -0,0 +1,19 @@ +# =================================================================== +# Activate this profile to enable TLS and HTTP/2. +# +# JHipster has generated a self-signed certificate, which will be used to encrypt traffic. +# As your browser will not understand this certificate, you will need to import it. +# +# Another (easiest) solution with Chrome is to enable the "allow-insecure-localhost" flag +# at chrome://flags/#allow-insecure-localhost +# =================================================================== +server: + ssl: + key-store: classpath:config/tls/keystore.p12 + key-store-password: password + key-store-type: PKCS12 + key-alias: selfsigned + ciphers: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384, TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, TLS_DHE_RSA_WITH_AES_256_GCM_SHA384, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_256_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA256, TLS_DHE_RSA_WITH_AES_256_CBC_SHA256 + enabled-protocols: TLSv1.2 + http2: + enabled: true diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml new file mode 100644 index 0000000..e597415 --- /dev/null +++ b/src/main/resources/config/application.yml @@ -0,0 +1,180 @@ +# =================================================================== +# Spring Boot configuration. +# +# This configuration will be overridden by the Spring profile you use, +# for example application-dev.yml if you use the "dev" profile. +# +# More information on profiles: https://www.jhipster.tech/profiles/ +# More information on configuration properties: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# =================================================================== +# Standard Spring Boot properties. +# Full reference is available at: +# http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html +# =================================================================== + +management: + endpoints: + web: + base-path: /management + exposure: + include: + ['configprops', 'env', 'health', 'info', 'jhimetrics', 'logfile', 'loggers', 'prometheus', 'threaddump', 'caches', 'liquibase'] + endpoint: + health: + show-details: when_authorized + roles: 'ROLE_ADMIN' + probes: + enabled: true + jhimetrics: + enabled: true + info: + git: + mode: full + health: + group: + liveness: + include: livenessState + readiness: + include: readinessState,datasource + mail: + enabled: false # When using the MailService, configure an SMTP server and set this to true + metrics: + export: + # Prometheus is the default metrics backend + prometheus: + enabled: true + step: 60 + enable: + http: true + jvm: true + logback: true + process: true + system: true + distribution: + percentiles-histogram: + all: true + percentiles: + all: 0, 0.5, 0.75, 0.95, 0.99, 1.0 + tags: + application: ${spring.application.name} + web: + server: + request: + autotime: + enabled: true + +spring: + application: + name: store + profiles: + # The commented value for `active` can be replaced with valid Spring profiles to load. + # Otherwise, it will be filled in by gradle when building the JAR file + # Either way, it can be overridden by `--spring.profiles.active` value passed in the commandline or `-Dspring.profiles.active` set in `JAVA_OPTS` + active: #spring.profiles.active# + group: + dev: + - dev + - api-docs + # Uncomment to activate TLS for the dev profile + #- tls + jmx: + enabled: false + data: + jpa: + repositories: + bootstrap-mode: deferred + jpa: + open-in-view: false + properties: + hibernate.jdbc.time_zone: UTC + hibernate.id.new_generator_mappings: true + hibernate.connection.provider_disables_autocommit: true + hibernate.cache.use_second_level_cache: true + hibernate.cache.use_query_cache: false + hibernate.generate_statistics: false + # modify batch size as necessary + hibernate.jdbc.batch_size: 25 + hibernate.order_inserts: true + hibernate.order_updates: true + hibernate.query.fail_on_pagination_over_collection_fetch: true + hibernate.query.in_clause_parameter_padding: true + hibernate: + ddl-auto: none + naming: + physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy + implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + messages: + basename: i18n/messages + main: + allow-bean-definition-overriding: true + task: + execution: + thread-name-prefix: store-task- + pool: + core-size: 2 + max-size: 50 + queue-capacity: 10000 + scheduling: + thread-name-prefix: store-scheduling- + pool: + size: 2 + thymeleaf: + mode: HTML + output: + ansi: + console-available: true + +server: + servlet: + session: + cookie: + http-only: true + +# Properties to be exposed on the /info management endpoint +info: + # Comma separated list of profiles that will trigger the ribbon to show + display-ribbon-on-profiles: 'dev' + +# =================================================================== +# JHipster specific properties +# +# Full reference is available at: https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +jhipster: + clientApp: + name: 'storeApp' + # By default CORS is disabled. Uncomment to enable. + # cors: + # allowed-origins: "*" + # allowed-methods: "*" + # allowed-headers: "*" + # exposed-headers: "Authorization,Link,X-Total-Count,X-${jhipster.clientApp.name}-alert,X-${jhipster.clientApp.name}-error,X-${jhipster.clientApp.name}-params" + # allow-credentials: true + # max-age: 1800 + mail: + from: store@localhost + api-docs: + default-include-pattern: ${server.servlet.context-path:}/api/.* + management-include-pattern: ${server.servlet.context-path:}/management/.* + title: store API + description: store API documentation + version: 0.0.1 + terms-of-service-url: + contact-name: + contact-url: + contact-email: + license: unlicensed + license-url: +# =================================================================== +# Application specific properties +# Add your own application properties here, see the ApplicationProperties class +# to have type-safe configuration, like in the JHipsterProperties above +# +# More documentation is available at: +# https://www.jhipster.tech/common-application-properties/ +# =================================================================== + +# application: diff --git a/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml b/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml new file mode 100644 index 0000000..8d200bf --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20200424080100_added_entity_Product.xml b/src/main/resources/config/liquibase/changelog/20200424080100_added_entity_Product.xml new file mode 100644 index 0000000..ba363ed --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20200424080100_added_entity_Product.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20200424080100_added_entity_constraints_Product.xml b/src/main/resources/config/liquibase/changelog/20200424080100_added_entity_constraints_Product.xml new file mode 100644 index 0000000..3fac7c8 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20200424080100_added_entity_constraints_Product.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20200424080200_added_entity_ProductCategory.xml b/src/main/resources/config/liquibase/changelog/20200424080200_added_entity_ProductCategory.xml new file mode 100644 index 0000000..7dd43f5 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20200424080200_added_entity_ProductCategory.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20200424080300_added_entity_CustomerDetails.xml b/src/main/resources/config/liquibase/changelog/20200424080300_added_entity_CustomerDetails.xml new file mode 100644 index 0000000..17c5caa --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20200424080300_added_entity_CustomerDetails.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20200424080300_added_entity_constraints_CustomerDetails.xml b/src/main/resources/config/liquibase/changelog/20200424080300_added_entity_constraints_CustomerDetails.xml new file mode 100644 index 0000000..731bb04 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20200424080300_added_entity_constraints_CustomerDetails.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20200424080400_added_entity_ShoppingCart.xml b/src/main/resources/config/liquibase/changelog/20200424080400_added_entity_ShoppingCart.xml new file mode 100644 index 0000000..ae7e52a --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20200424080400_added_entity_ShoppingCart.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20200424080400_added_entity_constraints_ShoppingCart.xml b/src/main/resources/config/liquibase/changelog/20200424080400_added_entity_constraints_ShoppingCart.xml new file mode 100644 index 0000000..b0c72f8 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20200424080400_added_entity_constraints_ShoppingCart.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20200424080500_added_entity_ProductOrder.xml b/src/main/resources/config/liquibase/changelog/20200424080500_added_entity_ProductOrder.xml new file mode 100644 index 0000000..9481cb5 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20200424080500_added_entity_ProductOrder.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/changelog/20200424080500_added_entity_constraints_ProductOrder.xml b/src/main/resources/config/liquibase/changelog/20200424080500_added_entity_constraints_ProductOrder.xml new file mode 100644 index 0000000..55931dc --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20200424080500_added_entity_constraints_ProductOrder.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/src/main/resources/config/liquibase/data/authority.csv b/src/main/resources/config/liquibase/data/authority.csv new file mode 100644 index 0000000..af5c6df --- /dev/null +++ b/src/main/resources/config/liquibase/data/authority.csv @@ -0,0 +1,3 @@ +name +ROLE_ADMIN +ROLE_USER diff --git a/src/main/resources/config/liquibase/data/user.csv b/src/main/resources/config/liquibase/data/user.csv new file mode 100644 index 0000000..fbc52da --- /dev/null +++ b/src/main/resources/config/liquibase/data/user.csv @@ -0,0 +1,3 @@ +id;login;password_hash;first_name;last_name;email;image_url;activated;lang_key;created_by;last_modified_by +1;admin;$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC;Administrator;Administrator;admin@localhost;;true;en;system;system +2;user;$2a$10$VEjxo0jq2YG9Rbk2HmX9S.k1uZBGYUHdUcid3g/vfiEl7lwWgOH/K;User;User;user@localhost;;true;en;system;system diff --git a/src/main/resources/config/liquibase/data/user_authority.csv b/src/main/resources/config/liquibase/data/user_authority.csv new file mode 100644 index 0000000..01dbdef --- /dev/null +++ b/src/main/resources/config/liquibase/data/user_authority.csv @@ -0,0 +1,4 @@ +user_id;authority_name +1;ROLE_ADMIN +1;ROLE_USER +2;ROLE_USER diff --git a/src/main/resources/config/liquibase/fake-data/blob/hipster.png b/src/main/resources/config/liquibase/fake-data/blob/hipster.png new file mode 100644 index 0000000000000000000000000000000000000000..5b7d50756b00d556bd39a082e16e28a041caa150 GIT binary patch literal 7564 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/config/liquibase/fake-data/customer_details.csv b/src/main/resources/config/liquibase/fake-data/customer_details.csv new file mode 100644 index 0000000..6b5bce2 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/customer_details.csv @@ -0,0 +1,3 @@ +id;gender;phone;address_line_1;address_line_2;city;country;user_id +1;MALE;(819) 531-0580 x18595;Metal;Credit blue Representative;North Mireyahaven;Cameroon;1 +2;MALE;829.573.0287 x2699;redundant;Principal quantify;Haleyshire;Solomon Islands;2 diff --git a/src/main/resources/config/liquibase/fake-data/product.csv b/src/main/resources/config/liquibase/fake-data/product.csv new file mode 100644 index 0000000..d94a93d --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/product.csv @@ -0,0 +1,11 @@ +id;name;description;price;item_size;image;image_content_type;product_category_id +1;Steel;Assistant;57374;XXL;../fake-data/blob/hipster.png;image/png;1 +2;support navigate;Chilean reboot Dynamic;70131;L;../fake-data/blob/hipster.png;image/png;2 +3;withdrawal THX Division;Metal;83023;L;../fake-data/blob/hipster.png;image/png;3 +4;Michigan throughput Union;intermediate initiative Generic;32770;L;../fake-data/blob/hipster.png;image/png;4 +5;Ball Small;Computer CFP orange;9548;L;../fake-data/blob/hipster.png;image/png;5 +6;mobile Vermont auxiliary;COM Implementation Awesome;52784;M;../fake-data/blob/hipster.png;image/png;6 +7;withdrawal invoice;Bike Shoes Center;20319;M;../fake-data/blob/hipster.png;image/png;7 +8;pixel methodical quantifying;rich South Forward;79521;M;../fake-data/blob/hipster.png;image/png;8 +9;Computer Arizona mobile;AGP lavender;71562;XL;../fake-data/blob/hipster.png;image/png;9 +10;grey solution;Massachusetts Ergonomic Ergonomic;5859;S;../fake-data/blob/hipster.png;image/png;10 diff --git a/src/main/resources/config/liquibase/fake-data/product_category.csv b/src/main/resources/config/liquibase/fake-data/product_category.csv new file mode 100644 index 0000000..ec14631 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/product_category.csv @@ -0,0 +1,11 @@ +id;name;description +1;firmware;Soft proactive front-end +2;Strategist;capacitor cross-platform Lead +3;Bike circuit;Avon +4;COM Avon;card connecting +5;Palau;Fresh +6;Chair multi-byte;Buckinghamshire virtual +7;calculating;quantify +8;Universal reintermediate;Jersey payment Advanced +9;PNG vertical Afghani;interactive Togo +10;Brunei Guernsey;Movies diff --git a/src/main/resources/config/liquibase/fake-data/product_order.csv b/src/main/resources/config/liquibase/fake-data/product_order.csv new file mode 100644 index 0000000..2f3fe35 --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/product_order.csv @@ -0,0 +1,11 @@ +id;quantity;total_price;product_id;cart_id +1;34384;7124;1;1 +2;11446;1494;2;2 +3;18716;94428;3;3 +4;57858;19246;4;4 +5;75399;64871;5;5 +6;65977;39674;6;6 +7;88795;35999;7;7 +8;34472;21696;8;8 +9;42917;89918;9;9 +10;14185;91159;10;10 diff --git a/src/main/resources/config/liquibase/fake-data/shopping_cart.csv b/src/main/resources/config/liquibase/fake-data/shopping_cart.csv new file mode 100644 index 0000000..fee45ca --- /dev/null +++ b/src/main/resources/config/liquibase/fake-data/shopping_cart.csv @@ -0,0 +1,11 @@ +id;placed_date;status;total_price;payment_method;payment_reference;payment_modification_reference;customer_details_id +1;2020-04-23T19:19:50;OPEN;33498;IDEAL;Cambridgeshire invoice Buckinghamshire;withdrawal Configuration Multi-channelled;1 +2;2020-04-24T00:43:18;PENDING;94970;CREDIT_CARD;Developer;program;2 +3;2020-04-23T19:14:33;REFUND_FAILED;6346;IDEAL;COM;Ford;1 +4;2020-04-23T15:19:23;CANCELLED;87708;CREDIT_CARD;Branding enhance;generate invoice;1 +5;2020-04-24T07:30:21;REFUND_INITIATED;46169;CREDIT_CARD;bleeding-edge;port;1 +6;2020-04-23T18:42:52;PENDING;80;CREDIT_CARD;Small Music service-desk;Steel Car;1 +7;2020-04-24T03:55:41;CANCELLED;26697;IDEAL;Cliff;markets;1 +8;2020-04-23T14:13:37;CANCELLED;34860;IDEAL;neural withdrawal;aggregate;2 +9;2020-04-23T09:15:12;PAID;25972;IDEAL;Loan;Corporate Maryland;2 +10;2020-04-23T20:11:59;CANCELLED;17262;CREDIT_CARD;mission-critical schemas salmon;Programmable SMTP;1 diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml new file mode 100644 index 0000000..fb17493 --- /dev/null +++ b/src/main/resources/config/liquibase/master.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties new file mode 100644 index 0000000..3ae0e81 --- /dev/null +++ b/src/main/resources/i18n/messages.properties @@ -0,0 +1,21 @@ +# Error page +error.title=Your request cannot be processed +error.subtitle=Sorry, an error has occurred. +error.status=Status: +error.message=Message: + +# Activation email +email.activation.title=store account activation is required +email.activation.greeting=Dear {0} +email.activation.text1=Your store account has been created, please click on the URL below to activate it: +email.activation.text2=Regards, +email.signature=store Team. + +# Creation email +email.creation.text1=Your store account has been created, please click on the URL below to access it: + +# Reset email +email.reset.title=store password reset +email.reset.greeting=Dear {0} +email.reset.text1=For your store account a password reset was requested, please click on the URL below to reset it: +email.reset.text2=Regards, diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..c932884 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html new file mode 100644 index 0000000..690e856 --- /dev/null +++ b/src/main/resources/templates/error.html @@ -0,0 +1,92 @@ + + + + + + Your request cannot be processed + + + +

+

Your request cannot be processed :(

+ +

Sorry, an error has occurred.

+ + Status:  ()
+ + Message: 
+
+
+ + diff --git a/src/main/resources/templates/mail/activationEmail.html b/src/main/resources/templates/mail/activationEmail.html new file mode 100644 index 0000000..0bb53a2 --- /dev/null +++ b/src/main/resources/templates/mail/activationEmail.html @@ -0,0 +1,20 @@ + + + + JHipster activation + + + + +

Dear

+

Your JHipster account has been created, please click on the URL below to activate it:

+

+ Activation link +

+

+ Regards, +
+ JHipster. +

+ + diff --git a/src/main/resources/templates/mail/creationEmail.html b/src/main/resources/templates/mail/creationEmail.html new file mode 100644 index 0000000..4e52898 --- /dev/null +++ b/src/main/resources/templates/mail/creationEmail.html @@ -0,0 +1,20 @@ + + + + JHipster creation + + + + +

Dear

+

Your JHipster account has been created, please click on the URL below to access it:

+

+ Login link +

+

+ Regards, +
+ JHipster. +

+ + diff --git a/src/main/resources/templates/mail/passwordResetEmail.html b/src/main/resources/templates/mail/passwordResetEmail.html new file mode 100644 index 0000000..290ca6d --- /dev/null +++ b/src/main/resources/templates/mail/passwordResetEmail.html @@ -0,0 +1,22 @@ + + + + JHipster password reset + + + + +

Dear

+

+ For your JHipster account a password reset was requested, please click on the URL below to reset it: +

+

+ Login link +

+

+ Regards, +
+ JHipster. +

+ + diff --git a/src/main/webapp/404.html b/src/main/webapp/404.html new file mode 100644 index 0000000..7569d7e --- /dev/null +++ b/src/main/webapp/404.html @@ -0,0 +1,58 @@ + + + + + Page Not Found + + + + + +

Page Not Found

+

Sorry, but the page you were trying to view does not exist.

+ + + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..f1611b5 --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,13 @@ + + + + + html + text/html;charset=utf-8 + + + diff --git a/src/main/webapp/app/_bootstrap-variables.scss b/src/main/webapp/app/_bootstrap-variables.scss new file mode 100644 index 0000000..83b0d5b --- /dev/null +++ b/src/main/webapp/app/_bootstrap-variables.scss @@ -0,0 +1,28 @@ +/* +* Bootstrap overrides https://v4-alpha.getbootstrap.com/getting-started/options/ +* All values defined in bootstrap source +* https://github.com/twbs/bootstrap/blob/v4-dev/scss/_variables.scss can be overwritten here +* Make sure not to add !default to values here +*/ + +// Options: +// Quickly modify global styling by enabling or disabling optional features. +$enable-rounded: true; +$enable-shadows: false; +$enable-gradients: false; +$enable-transitions: true; +$enable-hover-media-query: false; +$enable-grid-classes: true; +$enable-print-styles: true; + +// Components: +// Define common padding and border radius sizes and more. + +$border-radius: 0.15rem; +$border-radius-lg: 0.125rem; +$border-radius-sm: 0.1rem; + +// Body: +// Settings for the `` element. + +$body-bg: #e4e5e6; diff --git a/src/main/webapp/app/app.scss b/src/main/webapp/app/app.scss new file mode 100644 index 0000000..3e58105 --- /dev/null +++ b/src/main/webapp/app/app.scss @@ -0,0 +1,312 @@ +// Override Boostrap variables +// Import Bootstrap source files from node_modules +@import 'node_modules/bootstrap/scss/bootstrap'; +body { + background: #fafafa; + margin: 0; +} + +a { + color: #533f03; + font-weight: bold; +} + +* { + -moz-box-sizing: border-box; + box-sizing: border-box; + &:after, + &::before { + -moz-box-sizing: border-box; + box-sizing: border-box; + } +} + +.app-container { + box-sizing: border-box; + .view-container { + width: 100%; + height: calc(100% - 40px); + overflow-y: auto; + overflow-x: hidden; + padding: 1rem; + .card { + padding: 1rem; + } + .view-routes { + height: 100%; + > div { + height: 100%; + } + } + } +} + +.fullscreen { + position: fixed; + top: 100px; + left: 0px; + width: 99% !important; + height: calc(100vh - 110px) !important; + margin: 5px; + z-index: 1000; + padding: 5px 25px 50px 25px !important; +} + +/* ========================================================================== +Browser Upgrade Prompt +========================================================================== */ + +.browserupgrade { + margin: 0.2em 0; + background: #ccc; + color: #000; + padding: 0.2em 0; +} + +/* ========================================================================== +Custom button styles +========================================================================== */ + +.icon-button > .btn { + background-color: transparent; + border-color: transparent; + padding: 0.5rem; + line-height: 1rem; + &:hover { + background-color: transparent; + border-color: transparent; + } + &:focus { + -webkit-box-shadow: none; + box-shadow: none; + } +} + +/* ========================================================================== +Generic styles +========================================================================== */ + +/* Temporary workaround for availity-reactstrap-validation */ +.invalid-feedback { + display: inline; +} + +/* other generic styles */ + +.title { + font-size: 1.25em; + margin: 1px 10px 1px 10px; +} + +.description { + font-size: 0.9em; + margin: 1px 10px 1px 10px; +} + +.shadow { + box-shadow: rgba(0, 0, 0, 0.12) 0px 1px 6px, rgba(0, 0, 0, 0.12) 0px 1px 4px; + border-radius: 2px; +} + +.error { + color: white; + background-color: red; +} + +.break { + white-space: normal; + word-break: break-all; +} + +.break-word { + white-space: normal; + word-break: keep-all; +} + +.preserve-space { + white-space: pre-wrap; +} + +/* padding helpers */ + +@mixin pad($size, $side) { + @if $size== '' { + @if $side== '' { + .pad { + padding: 10px !important; + } + } @else { + .pad { + padding-#{$side}: 10px !important; + } + } + } @else { + @if $side== '' { + .pad-#{$size} { + padding: #{$size}px !important; + } + } @else { + .pad-#{$side}-#{$size} { + padding-#{$side}: #{$size}px !important; + } + } + } +} + +@include pad('', ''); +@include pad('2', ''); +@include pad('3', ''); +@include pad('5', ''); +@include pad('10', ''); +@include pad('20', ''); +@include pad('25', ''); +@include pad('30', ''); +@include pad('50', ''); +@include pad('75', ''); +@include pad('100', ''); +@include pad('4', 'top'); +@include pad('5', 'top'); +@include pad('10', 'top'); +@include pad('20', 'top'); +@include pad('25', 'top'); +@include pad('30', 'top'); +@include pad('50', 'top'); +@include pad('75', 'top'); +@include pad('100', 'top'); +@include pad('4', 'bottom'); +@include pad('5', 'bottom'); +@include pad('10', 'bottom'); +@include pad('20', 'bottom'); +@include pad('25', 'bottom'); +@include pad('30', 'bottom'); +@include pad('50', 'bottom'); +@include pad('75', 'bottom'); +@include pad('100', 'bottom'); +@include pad('5', 'right'); +@include pad('10', 'right'); +@include pad('20', 'right'); +@include pad('25', 'right'); +@include pad('30', 'right'); +@include pad('50', 'right'); +@include pad('75', 'right'); +@include pad('100', 'right'); +@include pad('5', 'left'); +@include pad('10', 'left'); +@include pad('20', 'left'); +@include pad('25', 'left'); +@include pad('30', 'left'); +@include pad('50', 'left'); +@include pad('75', 'left'); +@include pad('100', 'left'); + +@mixin no-padding($side) { + @if $side== 'all' { + .no-padding { + padding: 0 !important; + } + } @else { + .no-padding-#{$side} { + padding-#{$side}: 0 !important; + } + } +} + +@include no-padding('left'); +@include no-padding('right'); +@include no-padding('top'); +@include no-padding('bottom'); +@include no-padding('all'); + +/* end of padding helpers */ + +.no-margin { + margin: 0px; +} +@mixin voffset($size) { + @if $size== '' { + .voffset { + margin-top: 2px !important; + } + } @else { + .voffset-#{$size} { + margin-top: #{$size}px !important; + } + } +} + +@include voffset(''); +@include voffset('5'); +@include voffset('10'); +@include voffset('15'); +@include voffset('30'); +@include voffset('40'); +@include voffset('60'); +@include voffset('80'); +@include voffset('100'); +@include voffset('150'); + +.readonly { + background-color: #eee; + opacity: 1; +} + +/* ========================================================================== +make sure browsers use the pointer cursor for anchors, even with no href +========================================================================== */ + +a:hover { + cursor: pointer; +} + +.hand { + cursor: pointer; +} + +button.anchor-btn { + background: none; + border: none; + padding: 0; + align-items: initial; + text-align: initial; + width: 100%; +} + +a.anchor-btn:hover { + text-decoration: none; +} + +/* ========================================================================== +Metrics and Health styles +========================================================================== */ + +#threadDump .popover, +#healthCheck .popover { + top: inherit; + display: block; + font-size: 10px; + max-width: 1024px; +} + +.thread-dump-modal-lock { + max-width: 450px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#healthCheck .popover { + margin-left: -50px; +} + +.health-details { + min-width: 400px; +} + +/* bootstrap 3 input-group 100% width +http://stackoverflow.com/questions/23436430/bootstrap-3-input-group-100-width */ + +.width-min { + width: 1% !important; +} + +/* jhipster-needle-scss-add-main JHipster will add new css style */ diff --git a/src/main/webapp/app/app.tsx b/src/main/webapp/app/app.tsx new file mode 100644 index 0000000..78b3bf4 --- /dev/null +++ b/src/main/webapp/app/app.tsx @@ -0,0 +1,72 @@ +import 'react-toastify/dist/ReactToastify.css'; +import './app.scss'; +import 'app/config/dayjs.ts'; + +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Card } from 'reactstrap'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { ToastContainer, toast } from 'react-toastify'; +import { hot } from 'react-hot-loader'; + +import { IRootState } from 'app/shared/reducers'; +import { getSession } from 'app/shared/reducers/authentication'; +import { getProfile } from 'app/shared/reducers/application-profile'; +import Header from 'app/shared/layout/header/header'; +import Footer from 'app/shared/layout/footer/footer'; +import { hasAnyAuthority } from 'app/shared/auth/private-route'; +import ErrorBoundary from 'app/shared/error/error-boundary'; +import { AUTHORITIES } from 'app/config/constants'; +import AppRoutes from 'app/routes'; + +const baseHref = document.querySelector('base').getAttribute('href').replace(/\/$/, ''); + +export interface IAppProps extends StateProps, DispatchProps {} + +export const App = (props: IAppProps) => { + useEffect(() => { + props.getSession(); + props.getProfile(); + }, []); + + const paddingTop = '60px'; + return ( + +
+ + +
+ +
+ + + + + +
+
+
+
+ ); +}; + +const mapStateToProps = ({ authentication, applicationProfile }: IRootState) => ({ + isAuthenticated: authentication.isAuthenticated, + isAdmin: hasAnyAuthority(authentication.account.authorities, [AUTHORITIES.ADMIN]), + ribbonEnv: applicationProfile.ribbonEnv, + isInProduction: applicationProfile.inProduction, + isOpenAPIEnabled: applicationProfile.isOpenAPIEnabled, +}); + +const mapDispatchToProps = { getSession, getProfile }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(hot(module)(App)); diff --git a/src/main/webapp/app/config/axios-interceptor.spec.ts b/src/main/webapp/app/config/axios-interceptor.spec.ts new file mode 100644 index 0000000..51abdfb --- /dev/null +++ b/src/main/webapp/app/config/axios-interceptor.spec.ts @@ -0,0 +1,31 @@ +import axios from 'axios'; +import sinon from 'sinon'; + +import setupAxiosInterceptors from './axios-interceptor'; + +describe('Axios Interceptor', () => { + describe('setupAxiosInterceptors', () => { + const client = axios; + const onUnauthenticated = sinon.spy(); + setupAxiosInterceptors(onUnauthenticated); + + it('onRequestSuccess is called on fulfilled request', () => { + expect((client.interceptors.request as any).handlers[0].fulfilled({ data: 'foo', url: '/test' })).toMatchObject({ + data: 'foo', + }); + }); + it('onResponseSuccess is called on fulfilled response', () => { + expect((client.interceptors.response as any).handlers[0].fulfilled({ data: 'foo' })).toEqual({ data: 'foo' }); + }); + it('onResponseError is called on rejected response', () => { + (client.interceptors.response as any).handlers[0].rejected({ + response: { + statusText: 'NotFound', + status: 403, + data: { message: 'Page not found' }, + }, + }); + expect(onUnauthenticated.calledOnce).toBe(true); + }); + }); +}); diff --git a/src/main/webapp/app/config/axios-interceptor.ts b/src/main/webapp/app/config/axios-interceptor.ts new file mode 100644 index 0000000..483a673 --- /dev/null +++ b/src/main/webapp/app/config/axios-interceptor.ts @@ -0,0 +1,30 @@ +import axios from 'axios'; +import { Storage } from 'react-jhipster'; + +import { SERVER_API_URL } from 'app/config/constants'; + +const TIMEOUT = 1 * 60 * 1000; +axios.defaults.timeout = TIMEOUT; +axios.defaults.baseURL = SERVER_API_URL; + +const setupAxiosInterceptors = onUnauthenticated => { + const onRequestSuccess = config => { + const token = Storage.local.get('jhi-authenticationToken') || Storage.session.get('jhi-authenticationToken'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }; + const onResponseSuccess = response => response; + const onResponseError = err => { + const status = err.status || (err.response ? err.response.status : 0); + if (status === 403 || status === 401) { + onUnauthenticated(); + } + return Promise.reject(err); + }; + axios.interceptors.request.use(onRequestSuccess); + axios.interceptors.response.use(onResponseSuccess, onResponseError); +}; + +export default setupAxiosInterceptors; diff --git a/src/main/webapp/app/config/constants.ts b/src/main/webapp/app/config/constants.ts new file mode 100644 index 0000000..33ae5e9 --- /dev/null +++ b/src/main/webapp/app/config/constants.ts @@ -0,0 +1,24 @@ +const config = { + VERSION: process.env.VERSION, +}; + +export default config; + +export const SERVER_API_URL = process.env.SERVER_API_URL; + +export const AUTHORITIES = { + ADMIN: 'ROLE_ADMIN', + USER: 'ROLE_USER', +}; + +export const messages = { + DATA_ERROR_ALERT: 'Internal Error', +}; + +export const APP_DATE_FORMAT = 'DD/MM/YY HH:mm'; +export const APP_TIMESTAMP_FORMAT = 'DD/MM/YY HH:mm:ss'; +export const APP_LOCAL_DATE_FORMAT = 'DD/MM/YYYY'; +export const APP_LOCAL_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm'; +export const APP_LOCAL_DATETIME_FORMAT_Z = 'YYYY-MM-DDTHH:mm Z'; +export const APP_WHOLE_NUMBER_FORMAT = '0,0'; +export const APP_TWO_DIGITS_AFTER_POINT_NUMBER_FORMAT = '0,0.[00]'; diff --git a/src/main/webapp/app/config/dayjs.ts b/src/main/webapp/app/config/dayjs.ts new file mode 100644 index 0000000..634a10b --- /dev/null +++ b/src/main/webapp/app/config/dayjs.ts @@ -0,0 +1,11 @@ +import dayjs from 'dayjs'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; +import duration from 'dayjs/plugin/duration'; +import relativeTime from 'dayjs/plugin/relativeTime'; + +// jhipster-needle-i18n-language-dayjs-imports - JHipster will import languages from dayjs here + +// DAYJS CONFIGURATION +dayjs.extend(customParseFormat); +dayjs.extend(duration); +dayjs.extend(relativeTime); diff --git a/src/main/webapp/app/config/devtools.tsx b/src/main/webapp/app/config/devtools.tsx new file mode 100644 index 0000000..47cfd76 --- /dev/null +++ b/src/main/webapp/app/config/devtools.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { createDevTools } from 'redux-devtools'; +import LogMonitor from 'redux-devtools-log-monitor'; +import DockMonitor from 'redux-devtools-dock-monitor'; +// You can toggle visibility of devTools with ctrl + H +// and change their position with ctrl + Q +export default createDevTools( + + + +); diff --git a/src/main/webapp/app/config/error-middleware.ts b/src/main/webapp/app/config/error-middleware.ts new file mode 100644 index 0000000..083ad0b --- /dev/null +++ b/src/main/webapp/app/config/error-middleware.ts @@ -0,0 +1,38 @@ +import { isPromise } from 'react-jhipster'; + +const getErrorMessage = errorData => { + let message = errorData.message; + if (errorData.fieldErrors) { + errorData.fieldErrors.forEach(fErr => { + message += `\nfield: ${fErr.field}, Object: ${fErr.objectName}, message: ${fErr.message}\n`; + }); + } + return message; +}; + +export default () => next => action => { + // If not a promise, continue on + if (!isPromise(action.payload)) { + return next(action); + } + + /** + * + * The error middleware serves to dispatch the initial pending promise to + * the promise middleware, but adds a `catch`. + * It need not run in production + */ + if (process.env.NODE_ENV === 'development') { + // Dispatch initial pending promise, but catch any errors + return next(action).catch(error => { + console.error(`${action.type} caught at middleware with reason: ${JSON.stringify(error.message)}.`); + if (error && error.response && error.response.data) { + const message = getErrorMessage(error.response.data); + console.error(`Actual cause: ${message}`); + } + + return Promise.reject(error); + }); + } + return next(action); +}; diff --git a/src/main/webapp/app/config/icon-loader.ts b/src/main/webapp/app/config/icon-loader.ts new file mode 100644 index 0000000..e52a1fa --- /dev/null +++ b/src/main/webapp/app/config/icon-loader.ts @@ -0,0 +1,73 @@ +import { faCogs } from '@fortawesome/free-solid-svg-icons/faCogs'; +import { faBan } from '@fortawesome/free-solid-svg-icons/faBan'; +import { faAsterisk } from '@fortawesome/free-solid-svg-icons/faAsterisk'; +import { faArrowLeft } from '@fortawesome/free-solid-svg-icons/faArrowLeft'; +import { faBell } from '@fortawesome/free-solid-svg-icons/faBell'; +import { faBook } from '@fortawesome/free-solid-svg-icons/faBook'; +import { faCloud } from '@fortawesome/free-solid-svg-icons/faCloud'; +import { faDatabase } from '@fortawesome/free-solid-svg-icons/faDatabase'; +import { faEye } from '@fortawesome/free-solid-svg-icons/faEye'; +import { faFlag } from '@fortawesome/free-solid-svg-icons/faFlag'; +import { faHeart } from '@fortawesome/free-solid-svg-icons/faHeart'; +import { faHome } from '@fortawesome/free-solid-svg-icons/faHome'; +import { faList } from '@fortawesome/free-solid-svg-icons/faList'; +import { faLock } from '@fortawesome/free-solid-svg-icons/faLock'; +import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt'; +import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus'; +import { faSave } from '@fortawesome/free-solid-svg-icons/faSave'; +import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch'; +import { faSort } from '@fortawesome/free-solid-svg-icons/faSort'; +import { faSync } from '@fortawesome/free-solid-svg-icons/faSync'; +import { faRoad } from '@fortawesome/free-solid-svg-icons/faRoad'; +import { faSignInAlt } from '@fortawesome/free-solid-svg-icons/faSignInAlt'; +import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons/faSignOutAlt'; +import { faTachometerAlt } from '@fortawesome/free-solid-svg-icons/faTachometerAlt'; +import { faTasks } from '@fortawesome/free-solid-svg-icons/faTasks'; +import { faThList } from '@fortawesome/free-solid-svg-icons/faThList'; +import { faTimesCircle } from '@fortawesome/free-solid-svg-icons/faTimesCircle'; +import { faTrash } from '@fortawesome/free-solid-svg-icons/faTrash'; +import { faUser } from '@fortawesome/free-solid-svg-icons/faUser'; +import { faUserPlus } from '@fortawesome/free-solid-svg-icons/faUserPlus'; +import { faUsers } from '@fortawesome/free-solid-svg-icons/faUsers'; +import { faUsersCog } from '@fortawesome/free-solid-svg-icons/faUsersCog'; +import { faWrench } from '@fortawesome/free-solid-svg-icons/faWrench'; + +import { library } from '@fortawesome/fontawesome-svg-core'; + +export const loadIcons = () => { + library.add( + faArrowLeft, + faAsterisk, + faBan, + faBell, + faBook, + faCloud, + faCogs, + faDatabase, + faEye, + faFlag, + faHeart, + faHome, + faList, + faLock, + faPencilAlt, + faPlus, + faRoad, + faSave, + faSignInAlt, + faSignOutAlt, + faSearch, + faSort, + faSync, + faTachometerAlt, + faTasks, + faThList, + faTimesCircle, + faTrash, + faUser, + faUserPlus, + faUsers, + faUsersCog, + faWrench + ); +}; diff --git a/src/main/webapp/app/config/logger-middleware.ts b/src/main/webapp/app/config/logger-middleware.ts new file mode 100644 index 0000000..152e83d --- /dev/null +++ b/src/main/webapp/app/config/logger-middleware.ts @@ -0,0 +1,13 @@ +/* eslint no-console: off */ +export default () => next => action => { + if (process.env.NODE_ENV !== 'production') { + const { type, payload, meta } = action; + + console.groupCollapsed(type); + console.log('Payload:', payload); + console.log('Meta:', meta); + console.groupEnd(); + } + + return next(action); +}; diff --git a/src/main/webapp/app/config/notification-middleware.spec.ts b/src/main/webapp/app/config/notification-middleware.spec.ts new file mode 100644 index 0000000..d91fce5 --- /dev/null +++ b/src/main/webapp/app/config/notification-middleware.spec.ts @@ -0,0 +1,190 @@ +import { createStore, applyMiddleware } from 'redux'; +import promiseMiddleware from 'redux-promise-middleware'; +import * as toastify from 'react-toastify'; // synthetic default import doesn't work here due to mocking. +import sinon from 'sinon'; + +import notificationMiddleware from './notification-middleware'; + +describe('Notification Middleware', () => { + let store; + + const SUCCESS_TYPE = 'SUCCESS'; + const ERROR_TYPE = 'SUCCESS'; + const DEFAULT_SUCCESS_MESSAGE = 'fooSuccess'; + const DEFAULT_ERROR_MESSAGE = 'fooError'; + + // Default action for use in local tests + const DEFAULT = { + type: SUCCESS_TYPE, + payload: 'foo', + }; + const DEFAULT_PROMISE = { + type: SUCCESS_TYPE, + payload: Promise.resolve('foo'), + }; + const DEFAULT_SUCCESS = { + type: SUCCESS_TYPE, + meta: { + successMessage: DEFAULT_SUCCESS_MESSAGE, + }, + payload: Promise.resolve('foo'), + }; + const HEADER_SUCCESS = { + type: SUCCESS_TYPE, + payload: Promise.resolve({ + status: 201, + statusText: 'Created', + headers: { 'app-alert': 'foo.created', 'app-params': 'foo' }, + }), + }; + const DEFAULT_ERROR = { + type: ERROR_TYPE, + meta: { + errorMessage: DEFAULT_ERROR_MESSAGE, + }, + payload: Promise.reject(new Error('foo')), + }; + const VALIDATION_ERROR = { + type: ERROR_TYPE, + payload: Promise.reject({ + response: { + data: { + type: 'https://www.jhipster.tech/problem/constraint-violation', + title: 'Method argument not valid', + status: 400, + path: '/api/foos', + message: 'error.validation', + fieldErrors: [{ objectName: 'foos', field: 'minField', message: 'Min' }], + }, + status: 400, + statusText: 'Bad Request', + headers: { expires: '0' }, + }, + }), + }; + const HEADER_ERRORS = { + type: ERROR_TYPE, + payload: Promise.reject({ + response: { + status: 400, + statusText: 'Bad Request', + headers: { 'app-error': 'foo.creation', 'app-params': 'foo' }, + }, + }), + }; + const NOT_FOUND_ERROR = { + type: ERROR_TYPE, + payload: Promise.reject({ + response: { + data: { + status: 404, + message: 'Not found', + }, + status: 404, + }, + }), + }; + const NO_SERVER_ERROR = { + type: ERROR_TYPE, + payload: Promise.reject({ + response: { + status: 0, + }, + }), + }; + const GENERIC_ERROR = { + type: ERROR_TYPE, + payload: Promise.reject({ + response: { + data: { + message: 'Error', + }, + }, + }), + }; + + const makeStore = () => applyMiddleware(notificationMiddleware, promiseMiddleware)(createStore)(() => null); + + beforeEach(() => { + store = makeStore(); + sinon.spy(toastify.toast, 'error'); + sinon.spy(toastify.toast, 'success'); + }); + + afterEach(() => { + (toastify.toast as any).error.restore(); + (toastify.toast as any).success.restore(); + }); + + it('should not trigger a toast message but should return action', () => { + expect(store.dispatch(DEFAULT).payload).toEqual('foo'); + expect((toastify.toast as any).error.called).toEqual(false); + expect((toastify.toast as any).success.called).toEqual(false); + }); + + it('should not trigger a toast message but should return promise success', async () => { + await store.dispatch(DEFAULT_PROMISE).then(resp => { + expect(resp.value).toEqual('foo'); + }); + expect((toastify.toast as any).error.called).toEqual(false); + expect((toastify.toast as any).success.called).toEqual(false); + }); + + it('should trigger a success toast message and return promise success', async () => { + await store.dispatch(DEFAULT_SUCCESS).then(resp => { + expect(resp.value).toEqual('foo'); + }); + const toastMsg = (toastify.toast as any).success.getCall(0).args[0]; + expect(toastMsg).toEqual(DEFAULT_SUCCESS_MESSAGE); + }); + it('should trigger a success toast message and return promise success for header alerts', async () => { + await store.dispatch(HEADER_SUCCESS).then(resp => { + expect(resp.value.status).toEqual(201); + }); + const toastMsg = (toastify.toast as any).success.getCall(0).args[0]; + expect(toastMsg).toContain('foo.created'); + }); + + it('should trigger an error toast message and return promise error', async () => { + await store.dispatch(DEFAULT_ERROR).catch(err => { + expect(err.message).toEqual('foo'); + }); + const toastMsg = (toastify.toast as any).error.getCall(0).args[0]; + expect(toastMsg).toEqual(DEFAULT_ERROR_MESSAGE); + }); + it('should trigger an error toast message and return promise error for generic message', async () => { + await store.dispatch(GENERIC_ERROR).catch(err => { + expect(err.response.data.message).toEqual('Error'); + }); + const toastMsg = (toastify.toast as any).error.getCall(0).args[0]; + expect(toastMsg).toContain('Error'); + }); + it('should trigger an error toast message and return promise error for 400 response code', async () => { + await store.dispatch(VALIDATION_ERROR).catch(err => { + expect(err.response.data.message).toEqual('error.validation'); + }); + const toastMsg = (toastify.toast as any).error.getCall(0).args[0]; + expect(toastMsg).toContain('Error on field "MinField"'); + }); + it('should trigger an error toast message and return promise error for 404 response code', async () => { + await store.dispatch(NOT_FOUND_ERROR).catch(err => { + expect(err.response.data.message).toEqual('Not found'); + }); + const toastMsg = (toastify.toast as any).error.getCall(0).args[0]; + expect(toastMsg).toContain('Not found'); + }); + it('should trigger an error toast message and return promise error for 0 response code', async () => { + await store.dispatch(NO_SERVER_ERROR).catch(err => { + expect(err.response.status).toEqual(0); + }); + const toastMsg = (toastify.toast as any).error.getCall(0).args[0]; + expect(toastMsg).toContain('Server not reachable'); + }); + it('should trigger an error toast message and return promise error for headers containing errors', async () => { + await store.dispatch(HEADER_ERRORS).catch(err => { + expect(err.response.status).toEqual(400); + }); + const toastMsg = (toastify.toast as any).error.getCall(0).args[0]; + expect(toastMsg).toContain('foo.creation'); + }); +}); diff --git a/src/main/webapp/app/config/notification-middleware.ts b/src/main/webapp/app/config/notification-middleware.ts new file mode 100644 index 0000000..417ecca --- /dev/null +++ b/src/main/webapp/app/config/notification-middleware.ts @@ -0,0 +1,105 @@ +import { isPromise } from 'react-jhipster'; +import { toast } from 'react-toastify'; + +const addErrorAlert = (message, key?, data?) => { + toast.error(message); +}; +export default () => next => action => { + // If not a promise, continue on + if (!isPromise(action.payload)) { + return next(action); + } + + /** + * + * The notification middleware serves to dispatch the initial pending promise to + * the promise middleware, but adds a `then` and `catch. + */ + return next(action) + .then(response => { + if (action.meta && action.meta.successMessage) { + toast.success(action.meta.successMessage); + } else if (response && response.action && response.action.payload && response.action.payload.headers) { + const headers = response.action.payload.headers; + let alert: string | null = null; + Object.entries(headers).forEach(([k, v]) => { + if (k.toLowerCase().endsWith('app-alert')) { + alert = v; + } + }); + if (alert) { + toast.success(alert); + } + } + return Promise.resolve(response); + }) + .catch(error => { + if (action.meta && action.meta.errorMessage) { + toast.error(action.meta.errorMessage); + } else if (error && error.response) { + const response = error.response; + const data = response.data; + if (!(response.status === 401 && (error.message === '' || (data && data.path && data.path.includes('/api/account'))))) { + let i; + switch (response.status) { + // connection refused, server not reachable + case 0: + addErrorAlert('Server not reachable', 'error.server.not.reachable'); + break; + + case 400: { + const headers = Object.entries(response.headers); + let errorHeader: string | null = null; + let entityKey: string | null = null; + headers.forEach(([k, v]) => { + if (k.toLowerCase().endsWith('app-error')) { + errorHeader = v; + } else if (k.toLowerCase().endsWith('app-params')) { + entityKey = v; + } + }); + if (errorHeader) { + const entityName = entityKey; + addErrorAlert(errorHeader, errorHeader, { entityName }); + } else if (data !== '' && data.fieldErrors) { + const fieldErrors = data.fieldErrors; + for (i = 0; i < fieldErrors.length; i++) { + const fieldError = fieldErrors[i]; + if (['Min', 'Max', 'DecimalMin', 'DecimalMax'].includes(fieldError.message)) { + fieldError.message = 'Size'; + } + // convert 'something[14].other[4].id' to 'something[].other[].id' so translations can be written to it + const convertedField = fieldError.field.replace(/\[\d*\]/g, '[]'); + const fieldName = convertedField.charAt(0).toUpperCase() + convertedField.slice(1); + addErrorAlert(`Error on field "${fieldName}"`, `error.${fieldError.message}`, { fieldName }); + } + } else if (data !== '' && data.message) { + addErrorAlert(data.message, data.message, data.params); + } else { + addErrorAlert(data); + } + break; + } + case 404: + addErrorAlert('Not found', 'error.url.not.found'); + break; + + default: + if (data !== '' && data.message) { + addErrorAlert(data.message); + } else { + addErrorAlert(data); + } + } + } + } else if (error && error.config && error.config.url === 'api/account' && error.config.method === 'get') { + /* eslint-disable no-console */ + console.log('Authentication Error: Trying to access url api/account with GET.'); + } else if (error && error.message) { + toast.error(error.message); + } else { + toast.error('Unknown error!'); + } + return Promise.reject(error); + }); +}; diff --git a/src/main/webapp/app/config/store.ts b/src/main/webapp/app/config/store.ts new file mode 100644 index 0000000..e3d131b --- /dev/null +++ b/src/main/webapp/app/config/store.ts @@ -0,0 +1,26 @@ +import { createStore, applyMiddleware, compose } from 'redux'; +import promiseMiddleware from 'redux-promise-middleware'; +import thunkMiddleware from 'redux-thunk'; +import reducer, { IRootState } from 'app/shared/reducers'; +import DevTools from './devtools'; +import errorMiddleware from './error-middleware'; +import notificationMiddleware from './notification-middleware'; +import loggerMiddleware from './logger-middleware'; +import { loadingBarMiddleware } from 'react-redux-loading-bar'; + +const defaultMiddlewares = [ + thunkMiddleware, + errorMiddleware, + notificationMiddleware, + promiseMiddleware, + loadingBarMiddleware(), + loggerMiddleware, +]; +const composedMiddlewares = middlewares => + process.env.NODE_ENV === 'development' + ? compose(applyMiddleware(...defaultMiddlewares, ...middlewares), DevTools.instrument()) + : compose(applyMiddleware(...defaultMiddlewares, ...middlewares)); + +const initialize = (initialState?: IRootState, middlewares = []) => createStore(reducer, initialState, composedMiddlewares(middlewares)); + +export default initialize; diff --git a/src/main/webapp/app/entities/customer-details/customer-details-delete-dialog.tsx b/src/main/webapp/app/entities/customer-details/customer-details-delete-dialog.tsx new file mode 100644 index 0000000..18b6c3c --- /dev/null +++ b/src/main/webapp/app/entities/customer-details/customer-details-delete-dialog.tsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { RouteComponentProps } from 'react-router-dom'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntity, deleteEntity } from './customer-details.reducer'; + +export interface ICustomerDetailsDeleteDialogProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const CustomerDetailsDeleteDialog = (props: ICustomerDetailsDeleteDialogProps) => { + useEffect(() => { + props.getEntity(props.match.params.id); + }, []); + + const handleClose = () => { + props.history.push('/customer-details' + props.location.search); + }; + + useEffect(() => { + if (props.updateSuccess) { + handleClose(); + } + }, [props.updateSuccess]); + + const confirmDelete = () => { + props.deleteEntity(props.customerDetailsEntity.id); + }; + + const { customerDetailsEntity } = props; + return ( + + + Confirm delete operation + + Are you sure you want to delete this CustomerDetails? + + + + + + ); +}; + +const mapStateToProps = ({ customerDetails }: IRootState) => ({ + customerDetailsEntity: customerDetails.entity, + updateSuccess: customerDetails.updateSuccess, +}); + +const mapDispatchToProps = { getEntity, deleteEntity }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(CustomerDetailsDeleteDialog); diff --git a/src/main/webapp/app/entities/customer-details/customer-details-detail.tsx b/src/main/webapp/app/entities/customer-details/customer-details-detail.tsx new file mode 100644 index 0000000..60844e2 --- /dev/null +++ b/src/main/webapp/app/entities/customer-details/customer-details-detail.tsx @@ -0,0 +1,77 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Row, Col } from 'reactstrap'; +import {} from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntity } from './customer-details.reducer'; +import { APP_DATE_FORMAT, APP_LOCAL_DATE_FORMAT } from 'app/config/constants'; + +export interface ICustomerDetailsDetailProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const CustomerDetailsDetail = (props: ICustomerDetailsDetailProps) => { + useEffect(() => { + props.getEntity(props.match.params.id); + }, []); + + const { customerDetailsEntity } = props; + return ( + + +

CustomerDetails

+
+
+ ID +
+
{customerDetailsEntity.id}
+
+ Gender +
+
{customerDetailsEntity.gender}
+
+ Phone +
+
{customerDetailsEntity.phone}
+
+ Address Line 1 +
+
{customerDetailsEntity.addressLine1}
+
+ Address Line 2 +
+
{customerDetailsEntity.addressLine2}
+
+ City +
+
{customerDetailsEntity.city}
+
+ Country +
+
{customerDetailsEntity.country}
+
User
+
{customerDetailsEntity.user ? customerDetailsEntity.user.login : ''}
+
+ +   + + +
+ ); +}; + +const mapStateToProps = ({ customerDetails }: IRootState) => ({ + customerDetailsEntity: customerDetails.entity, +}); + +const mapDispatchToProps = { getEntity }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(CustomerDetailsDetail); diff --git a/src/main/webapp/app/entities/customer-details/customer-details-reducer.spec.ts b/src/main/webapp/app/entities/customer-details/customer-details-reducer.spec.ts new file mode 100644 index 0000000..d0bc936 --- /dev/null +++ b/src/main/webapp/app/entities/customer-details/customer-details-reducer.spec.ts @@ -0,0 +1,304 @@ +import axios from 'axios'; + +import configureStore from 'redux-mock-store'; +import promiseMiddleware from 'redux-promise-middleware'; +import thunk from 'redux-thunk'; +import sinon from 'sinon'; + +import reducer, { + ACTION_TYPES, + createEntity, + deleteEntity, + getEntities, + getEntity, + updateEntity, + partialUpdate, + reset, +} from './customer-details.reducer'; +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; +import { ICustomerDetails, defaultValue } from 'app/shared/model/customer-details.model'; + +describe('Entities reducer tests', () => { + function isEmpty(element): boolean { + if (element instanceof Array) { + return element.length === 0; + } else { + return Object.keys(element).length === 0; + } + } + + const initialState = { + loading: false, + errorMessage: null, + entities: [] as ReadonlyArray, + entity: defaultValue, + totalItems: 0, + updating: false, + updateSuccess: false, + }; + + function testInitialState(state) { + expect(state).toMatchObject({ + loading: false, + errorMessage: null, + updating: false, + updateSuccess: false, + }); + expect(isEmpty(state.entities)); + expect(isEmpty(state.entity)); + } + + function testMultipleTypes(types, payload, testFunction) { + types.forEach(e => { + testFunction(reducer(undefined, { type: e, payload })); + }); + } + + describe('Common', () => { + it('should return the initial state', () => { + testInitialState(reducer(undefined, {})); + }); + }); + + describe('Requests', () => { + it('should set state to loading', () => { + testMultipleTypes([REQUEST(ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST), REQUEST(ACTION_TYPES.FETCH_CUSTOMERDETAILS)], {}, state => { + expect(state).toMatchObject({ + errorMessage: null, + updateSuccess: false, + loading: true, + }); + }); + }); + + it('should set state to updating', () => { + testMultipleTypes( + [ + REQUEST(ACTION_TYPES.CREATE_CUSTOMERDETAILS), + REQUEST(ACTION_TYPES.UPDATE_CUSTOMERDETAILS), + REQUEST(ACTION_TYPES.PARTIAL_UPDATE_CUSTOMERDETAILS), + REQUEST(ACTION_TYPES.DELETE_CUSTOMERDETAILS), + ], + {}, + state => { + expect(state).toMatchObject({ + errorMessage: null, + updateSuccess: false, + updating: true, + }); + } + ); + }); + + it('should reset the state', () => { + expect( + reducer( + { ...initialState, loading: true }, + { + type: ACTION_TYPES.RESET, + } + ) + ).toEqual({ + ...initialState, + }); + }); + }); + + describe('Failures', () => { + it('should set a message in errorMessage', () => { + testMultipleTypes( + [ + FAILURE(ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST), + FAILURE(ACTION_TYPES.FETCH_CUSTOMERDETAILS), + FAILURE(ACTION_TYPES.CREATE_CUSTOMERDETAILS), + FAILURE(ACTION_TYPES.UPDATE_CUSTOMERDETAILS), + FAILURE(ACTION_TYPES.PARTIAL_UPDATE_CUSTOMERDETAILS), + FAILURE(ACTION_TYPES.DELETE_CUSTOMERDETAILS), + ], + 'error message', + state => { + expect(state).toMatchObject({ + errorMessage: 'error message', + updateSuccess: false, + updating: false, + }); + } + ); + }); + }); + + describe('Successes', () => { + it('should fetch all entities', () => { + const payload = { data: [{ 1: 'fake1' }, { 2: 'fake2' }], headers: { 'x-total-count': 123 } }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST), + payload, + }) + ).toEqual({ + ...initialState, + loading: false, + totalItems: payload.headers['x-total-count'], + entities: payload.data, + }); + }); + + it('should fetch a single entity', () => { + const payload = { data: { 1: 'fake1' } }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.FETCH_CUSTOMERDETAILS), + payload, + }) + ).toEqual({ + ...initialState, + loading: false, + entity: payload.data, + }); + }); + + it('should create/update entity', () => { + const payload = { data: 'fake payload' }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.CREATE_CUSTOMERDETAILS), + payload, + }) + ).toEqual({ + ...initialState, + updating: false, + updateSuccess: true, + entity: payload.data, + }); + }); + + it('should delete entity', () => { + const payload = 'fake payload'; + const toTest = reducer(undefined, { + type: SUCCESS(ACTION_TYPES.DELETE_CUSTOMERDETAILS), + payload, + }); + expect(toTest).toMatchObject({ + updating: false, + updateSuccess: true, + }); + }); + }); + + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + beforeEach(() => { + const mockStore = configureStore([thunk, promiseMiddleware]); + store = mockStore({}); + axios.get = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.post = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.put = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.patch = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.delete = sinon.stub().returns(Promise.resolve(resolvedObject)); + }); + + it('dispatches ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(getEntities()).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.FETCH_CUSTOMERDETAILS actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_CUSTOMERDETAILS), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_CUSTOMERDETAILS), + payload: resolvedObject, + }, + ]; + await store.dispatch(getEntity(42666)).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.CREATE_CUSTOMERDETAILS actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.CREATE_CUSTOMERDETAILS), + }, + { + type: SUCCESS(ACTION_TYPES.CREATE_CUSTOMERDETAILS), + payload: resolvedObject, + }, + { + type: REQUEST(ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(createEntity({ id: 456 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.UPDATE_CUSTOMERDETAILS actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.UPDATE_CUSTOMERDETAILS), + }, + { + type: SUCCESS(ACTION_TYPES.UPDATE_CUSTOMERDETAILS), + payload: resolvedObject, + }, + ]; + await store.dispatch(updateEntity({ id: 456 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.PARTIAL_UPDATE_CUSTOMERDETAILS actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.PARTIAL_UPDATE_CUSTOMERDETAILS), + }, + { + type: SUCCESS(ACTION_TYPES.PARTIAL_UPDATE_CUSTOMERDETAILS), + payload: resolvedObject, + }, + ]; + await store.dispatch(partialUpdate({ id: 1 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.DELETE_CUSTOMERDETAILS actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.DELETE_CUSTOMERDETAILS), + }, + { + type: SUCCESS(ACTION_TYPES.DELETE_CUSTOMERDETAILS), + payload: resolvedObject, + }, + { + type: REQUEST(ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(deleteEntity(42666)).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.RESET actions', async () => { + const expectedActions = [ + { + type: ACTION_TYPES.RESET, + }, + ]; + await store.dispatch(reset()); + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/src/main/webapp/app/entities/customer-details/customer-details-update.tsx b/src/main/webapp/app/entities/customer-details/customer-details-update.tsx new file mode 100644 index 0000000..c50d280 --- /dev/null +++ b/src/main/webapp/app/entities/customer-details/customer-details-update.tsx @@ -0,0 +1,211 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Row, Col, Label } from 'reactstrap'; +import { AvFeedback, AvForm, AvGroup, AvInput, AvField } from 'availity-reactstrap-validation'; +import { translate } from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IRootState } from 'app/shared/reducers'; + +import { IUser } from 'app/shared/model/user.model'; +import { getUsers } from 'app/modules/administration/user-management/user-management.reducer'; +import { getEntity, updateEntity, createEntity, reset } from './customer-details.reducer'; +import { ICustomerDetails } from 'app/shared/model/customer-details.model'; +import { convertDateTimeFromServer, convertDateTimeToServer, displayDefaultDateTime } from 'app/shared/util/date-utils'; +import { mapIdList } from 'app/shared/util/entity-utils'; + +export interface ICustomerDetailsUpdateProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const CustomerDetailsUpdate = (props: ICustomerDetailsUpdateProps) => { + const [isNew] = useState(!props.match.params || !props.match.params.id); + + const { customerDetailsEntity, users, loading, updating } = props; + + const handleClose = () => { + props.history.push('/customer-details' + props.location.search); + }; + + useEffect(() => { + if (isNew) { + props.reset(); + } else { + props.getEntity(props.match.params.id); + } + + props.getUsers(); + }, []); + + useEffect(() => { + if (props.updateSuccess) { + handleClose(); + } + }, [props.updateSuccess]); + + const saveEntity = (event, errors, values) => { + if (errors.length === 0) { + const entity = { + ...customerDetailsEntity, + ...values, + user: users.find(it => it.id.toString() === values.userId.toString()), + }; + + if (isNew) { + props.createEntity(entity); + } else { + props.updateEntity(entity); + } + } + }; + + return ( +
+ + +

+ Create or edit a CustomerDetails +

+ +
+ + + {loading ? ( +

Loading...

+ ) : ( + + {!isNew ? ( + + + + + ) : null} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )) + : null} + + This field is required. + + +   + + + )} + +
+
+ ); +}; + +const mapStateToProps = (storeState: IRootState) => ({ + users: storeState.userManagement.users, + customerDetailsEntity: storeState.customerDetails.entity, + loading: storeState.customerDetails.loading, + updating: storeState.customerDetails.updating, + updateSuccess: storeState.customerDetails.updateSuccess, +}); + +const mapDispatchToProps = { + getUsers, + getEntity, + updateEntity, + createEntity, + reset, +}; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(CustomerDetailsUpdate); diff --git a/src/main/webapp/app/entities/customer-details/customer-details.reducer.ts b/src/main/webapp/app/entities/customer-details/customer-details.reducer.ts new file mode 100644 index 0000000..7181dea --- /dev/null +++ b/src/main/webapp/app/entities/customer-details/customer-details.reducer.ts @@ -0,0 +1,161 @@ +import axios from 'axios'; +import { ICrudGetAction, ICrudGetAllAction, ICrudPutAction, ICrudDeleteAction } from 'react-jhipster'; + +import { cleanEntity } from 'app/shared/util/entity-utils'; +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; + +import { ICustomerDetails, defaultValue } from 'app/shared/model/customer-details.model'; + +export const ACTION_TYPES = { + FETCH_CUSTOMERDETAILS_LIST: 'customerDetails/FETCH_CUSTOMERDETAILS_LIST', + FETCH_CUSTOMERDETAILS: 'customerDetails/FETCH_CUSTOMERDETAILS', + CREATE_CUSTOMERDETAILS: 'customerDetails/CREATE_CUSTOMERDETAILS', + UPDATE_CUSTOMERDETAILS: 'customerDetails/UPDATE_CUSTOMERDETAILS', + PARTIAL_UPDATE_CUSTOMERDETAILS: 'customerDetails/PARTIAL_UPDATE_CUSTOMERDETAILS', + DELETE_CUSTOMERDETAILS: 'customerDetails/DELETE_CUSTOMERDETAILS', + RESET: 'customerDetails/RESET', +}; + +const initialState = { + loading: false, + errorMessage: null, + entities: [] as ReadonlyArray, + entity: defaultValue, + updating: false, + totalItems: 0, + updateSuccess: false, +}; + +export type CustomerDetailsState = Readonly; + +// Reducer + +export default (state: CustomerDetailsState = initialState, action): CustomerDetailsState => { + switch (action.type) { + case REQUEST(ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST): + case REQUEST(ACTION_TYPES.FETCH_CUSTOMERDETAILS): + return { + ...state, + errorMessage: null, + updateSuccess: false, + loading: true, + }; + case REQUEST(ACTION_TYPES.CREATE_CUSTOMERDETAILS): + case REQUEST(ACTION_TYPES.UPDATE_CUSTOMERDETAILS): + case REQUEST(ACTION_TYPES.DELETE_CUSTOMERDETAILS): + case REQUEST(ACTION_TYPES.PARTIAL_UPDATE_CUSTOMERDETAILS): + return { + ...state, + errorMessage: null, + updateSuccess: false, + updating: true, + }; + case FAILURE(ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST): + case FAILURE(ACTION_TYPES.FETCH_CUSTOMERDETAILS): + case FAILURE(ACTION_TYPES.CREATE_CUSTOMERDETAILS): + case FAILURE(ACTION_TYPES.UPDATE_CUSTOMERDETAILS): + case FAILURE(ACTION_TYPES.PARTIAL_UPDATE_CUSTOMERDETAILS): + case FAILURE(ACTION_TYPES.DELETE_CUSTOMERDETAILS): + return { + ...state, + loading: false, + updating: false, + updateSuccess: false, + errorMessage: action.payload, + }; + case SUCCESS(ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST): + return { + ...state, + loading: false, + entities: action.payload.data, + totalItems: parseInt(action.payload.headers['x-total-count'], 10), + }; + case SUCCESS(ACTION_TYPES.FETCH_CUSTOMERDETAILS): + return { + ...state, + loading: false, + entity: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.CREATE_CUSTOMERDETAILS): + case SUCCESS(ACTION_TYPES.UPDATE_CUSTOMERDETAILS): + case SUCCESS(ACTION_TYPES.PARTIAL_UPDATE_CUSTOMERDETAILS): + return { + ...state, + updating: false, + updateSuccess: true, + entity: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.DELETE_CUSTOMERDETAILS): + return { + ...state, + updating: false, + updateSuccess: true, + entity: {}, + }; + case ACTION_TYPES.RESET: + return { + ...initialState, + }; + default: + return state; + } +}; + +const apiUrl = 'api/customer-details'; + +// Actions + +export const getEntities: ICrudGetAllAction = (page, size, sort) => { + const requestUrl = `${apiUrl}${sort ? `?page=${page}&size=${size}&sort=${sort}` : ''}`; + return { + type: ACTION_TYPES.FETCH_CUSTOMERDETAILS_LIST, + payload: axios.get(`${requestUrl}${sort ? '&' : '?'}cacheBuster=${new Date().getTime()}`), + }; +}; + +export const getEntity: ICrudGetAction = id => { + const requestUrl = `${apiUrl}/${id}`; + return { + type: ACTION_TYPES.FETCH_CUSTOMERDETAILS, + payload: axios.get(requestUrl), + }; +}; + +export const createEntity: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.CREATE_CUSTOMERDETAILS, + payload: axios.post(apiUrl, cleanEntity(entity)), + }); + dispatch(getEntities()); + return result; +}; + +export const updateEntity: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.UPDATE_CUSTOMERDETAILS, + payload: axios.put(`${apiUrl}/${entity.id}`, cleanEntity(entity)), + }); + return result; +}; + +export const partialUpdate: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.PARTIAL_UPDATE_CUSTOMERDETAILS, + payload: axios.patch(`${apiUrl}/${entity.id}`, cleanEntity(entity)), + }); + return result; +}; + +export const deleteEntity: ICrudDeleteAction = id => async dispatch => { + const requestUrl = `${apiUrl}/${id}`; + const result = await dispatch({ + type: ACTION_TYPES.DELETE_CUSTOMERDETAILS, + payload: axios.delete(requestUrl), + }); + dispatch(getEntities()); + return result; +}; + +export const reset = () => ({ + type: ACTION_TYPES.RESET, +}); diff --git a/src/main/webapp/app/entities/customer-details/customer-details.tsx b/src/main/webapp/app/entities/customer-details/customer-details.tsx new file mode 100644 index 0000000..3ed1982 --- /dev/null +++ b/src/main/webapp/app/entities/customer-details/customer-details.tsx @@ -0,0 +1,202 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Col, Row, Table } from 'reactstrap'; +import { Translate, getSortState, IPaginationBaseState, JhiPagination, JhiItemCount } from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntities } from './customer-details.reducer'; +import { ICustomerDetails } from 'app/shared/model/customer-details.model'; +import { APP_DATE_FORMAT, APP_LOCAL_DATE_FORMAT } from 'app/config/constants'; +import { ITEMS_PER_PAGE } from 'app/shared/util/pagination.constants'; +import { overridePaginationStateWithQueryParams } from 'app/shared/util/entity-utils'; + +export interface ICustomerDetailsProps extends StateProps, DispatchProps, RouteComponentProps<{ url: string }> {} + +export const CustomerDetails = (props: ICustomerDetailsProps) => { + const [paginationState, setPaginationState] = useState( + overridePaginationStateWithQueryParams(getSortState(props.location, ITEMS_PER_PAGE, 'id'), props.location.search) + ); + + const getAllEntities = () => { + props.getEntities(paginationState.activePage - 1, paginationState.itemsPerPage, `${paginationState.sort},${paginationState.order}`); + }; + + const sortEntities = () => { + getAllEntities(); + const endURL = `?page=${paginationState.activePage}&sort=${paginationState.sort},${paginationState.order}`; + if (props.location.search !== endURL) { + props.history.push(`${props.location.pathname}${endURL}`); + } + }; + + useEffect(() => { + sortEntities(); + }, [paginationState.activePage, paginationState.order, paginationState.sort]); + + useEffect(() => { + const params = new URLSearchParams(props.location.search); + const page = params.get('page'); + const sort = params.get('sort'); + if (page && sort) { + const sortSplit = sort.split(','); + setPaginationState({ + ...paginationState, + activePage: +page, + sort: sortSplit[0], + order: sortSplit[1], + }); + } + }, [props.location.search]); + + const sort = p => () => { + setPaginationState({ + ...paginationState, + order: paginationState.order === 'asc' ? 'desc' : 'asc', + sort: p, + }); + }; + + const handlePagination = currentPage => + setPaginationState({ + ...paginationState, + activePage: currentPage, + }); + + const handleSyncList = () => { + sortEntities(); + }; + + const { customerDetailsList, match, loading, totalItems } = props; + return ( +
+

+ Customer Details +
+ + + +   Create new Customer Details + +
+

+
+ {customerDetailsList && customerDetailsList.length > 0 ? ( + + + + + + + + + + + + + + + {customerDetailsList.map((customerDetails, i) => ( + + + + + + + + + + + + + ))} + +
+ ID + + Gender + + Phone + + Address Line 1 + + Address Line 2 + + City + + Country + + User + +
+ + {customerDetails.id}{customerDetails.gender}{customerDetails.phone}{customerDetails.addressLine1}{customerDetails.addressLine2}{customerDetails.city}{customerDetails.country}{customerDetails.user ? customerDetails.user.login : ''} +
+ + + +
+
+ ) : ( + !loading &&
No Customer Details found
+ )} +
+ {props.totalItems ? ( +
0 ? '' : 'd-none'}> + + + + + + +
+ ) : ( + '' + )} +
+ ); +}; + +const mapStateToProps = ({ customerDetails }: IRootState) => ({ + customerDetailsList: customerDetails.entities, + loading: customerDetails.loading, + totalItems: customerDetails.totalItems, +}); + +const mapDispatchToProps = { + getEntities, +}; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(CustomerDetails); diff --git a/src/main/webapp/app/entities/customer-details/index.tsx b/src/main/webapp/app/entities/customer-details/index.tsx new file mode 100644 index 0000000..74d19bc --- /dev/null +++ b/src/main/webapp/app/entities/customer-details/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Switch } from 'react-router-dom'; + +import ErrorBoundaryRoute from 'app/shared/error/error-boundary-route'; + +import CustomerDetails from './customer-details'; +import CustomerDetailsDetail from './customer-details-detail'; +import CustomerDetailsUpdate from './customer-details-update'; +import CustomerDetailsDeleteDialog from './customer-details-delete-dialog'; + +const Routes = ({ match }) => ( + <> + + + + + + + + +); + +export default Routes; diff --git a/src/main/webapp/app/entities/index.tsx b/src/main/webapp/app/entities/index.tsx new file mode 100644 index 0000000..bf19587 --- /dev/null +++ b/src/main/webapp/app/entities/index.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Switch } from 'react-router-dom'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import ErrorBoundaryRoute from 'app/shared/error/error-boundary-route'; + +import Product from './product'; +import ProductCategory from './product-category'; +import CustomerDetails from './customer-details'; +import ShoppingCart from './shopping-cart'; +import ProductOrder from './product-order'; +/* jhipster-needle-add-route-import - JHipster will add routes here */ + +const Routes = ({ match }) => ( +
+ + {/* prettier-ignore */} + + + + + + {/* jhipster-needle-add-route-path - JHipster will add routes here */} + +
+); + +export default Routes; diff --git a/src/main/webapp/app/entities/product-category/index.tsx b/src/main/webapp/app/entities/product-category/index.tsx new file mode 100644 index 0000000..39e5dbd --- /dev/null +++ b/src/main/webapp/app/entities/product-category/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Switch } from 'react-router-dom'; + +import ErrorBoundaryRoute from 'app/shared/error/error-boundary-route'; + +import ProductCategory from './product-category'; +import ProductCategoryDetail from './product-category-detail'; +import ProductCategoryUpdate from './product-category-update'; +import ProductCategoryDeleteDialog from './product-category-delete-dialog'; + +const Routes = ({ match }) => ( + <> + + + + + + + + +); + +export default Routes; diff --git a/src/main/webapp/app/entities/product-category/product-category-delete-dialog.tsx b/src/main/webapp/app/entities/product-category/product-category-delete-dialog.tsx new file mode 100644 index 0000000..79648d0 --- /dev/null +++ b/src/main/webapp/app/entities/product-category/product-category-delete-dialog.tsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { RouteComponentProps } from 'react-router-dom'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntity, deleteEntity } from './product-category.reducer'; + +export interface IProductCategoryDeleteDialogProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const ProductCategoryDeleteDialog = (props: IProductCategoryDeleteDialogProps) => { + useEffect(() => { + props.getEntity(props.match.params.id); + }, []); + + const handleClose = () => { + props.history.push('/product-category' + props.location.search); + }; + + useEffect(() => { + if (props.updateSuccess) { + handleClose(); + } + }, [props.updateSuccess]); + + const confirmDelete = () => { + props.deleteEntity(props.productCategoryEntity.id); + }; + + const { productCategoryEntity } = props; + return ( + + + Confirm delete operation + + Are you sure you want to delete this ProductCategory? + + + + + + ); +}; + +const mapStateToProps = ({ productCategory }: IRootState) => ({ + productCategoryEntity: productCategory.entity, + updateSuccess: productCategory.updateSuccess, +}); + +const mapDispatchToProps = { getEntity, deleteEntity }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ProductCategoryDeleteDialog); diff --git a/src/main/webapp/app/entities/product-category/product-category-detail.tsx b/src/main/webapp/app/entities/product-category/product-category-detail.tsx new file mode 100644 index 0000000..86bfe47 --- /dev/null +++ b/src/main/webapp/app/entities/product-category/product-category-detail.tsx @@ -0,0 +1,59 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Row, Col } from 'reactstrap'; +import {} from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntity } from './product-category.reducer'; +import { APP_DATE_FORMAT, APP_LOCAL_DATE_FORMAT } from 'app/config/constants'; + +export interface IProductCategoryDetailProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const ProductCategoryDetail = (props: IProductCategoryDetailProps) => { + useEffect(() => { + props.getEntity(props.match.params.id); + }, []); + + const { productCategoryEntity } = props; + return ( + + +

ProductCategory

+
+
+ ID +
+
{productCategoryEntity.id}
+
+ Name +
+
{productCategoryEntity.name}
+
+ Description +
+
{productCategoryEntity.description}
+
+ +   + + +
+ ); +}; + +const mapStateToProps = ({ productCategory }: IRootState) => ({ + productCategoryEntity: productCategory.entity, +}); + +const mapDispatchToProps = { getEntity }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ProductCategoryDetail); diff --git a/src/main/webapp/app/entities/product-category/product-category-reducer.spec.ts b/src/main/webapp/app/entities/product-category/product-category-reducer.spec.ts new file mode 100644 index 0000000..bd91a26 --- /dev/null +++ b/src/main/webapp/app/entities/product-category/product-category-reducer.spec.ts @@ -0,0 +1,304 @@ +import axios from 'axios'; + +import configureStore from 'redux-mock-store'; +import promiseMiddleware from 'redux-promise-middleware'; +import thunk from 'redux-thunk'; +import sinon from 'sinon'; + +import reducer, { + ACTION_TYPES, + createEntity, + deleteEntity, + getEntities, + getEntity, + updateEntity, + partialUpdate, + reset, +} from './product-category.reducer'; +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; +import { IProductCategory, defaultValue } from 'app/shared/model/product-category.model'; + +describe('Entities reducer tests', () => { + function isEmpty(element): boolean { + if (element instanceof Array) { + return element.length === 0; + } else { + return Object.keys(element).length === 0; + } + } + + const initialState = { + loading: false, + errorMessage: null, + entities: [] as ReadonlyArray, + entity: defaultValue, + totalItems: 0, + updating: false, + updateSuccess: false, + }; + + function testInitialState(state) { + expect(state).toMatchObject({ + loading: false, + errorMessage: null, + updating: false, + updateSuccess: false, + }); + expect(isEmpty(state.entities)); + expect(isEmpty(state.entity)); + } + + function testMultipleTypes(types, payload, testFunction) { + types.forEach(e => { + testFunction(reducer(undefined, { type: e, payload })); + }); + } + + describe('Common', () => { + it('should return the initial state', () => { + testInitialState(reducer(undefined, {})); + }); + }); + + describe('Requests', () => { + it('should set state to loading', () => { + testMultipleTypes([REQUEST(ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST), REQUEST(ACTION_TYPES.FETCH_PRODUCTCATEGORY)], {}, state => { + expect(state).toMatchObject({ + errorMessage: null, + updateSuccess: false, + loading: true, + }); + }); + }); + + it('should set state to updating', () => { + testMultipleTypes( + [ + REQUEST(ACTION_TYPES.CREATE_PRODUCTCATEGORY), + REQUEST(ACTION_TYPES.UPDATE_PRODUCTCATEGORY), + REQUEST(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTCATEGORY), + REQUEST(ACTION_TYPES.DELETE_PRODUCTCATEGORY), + ], + {}, + state => { + expect(state).toMatchObject({ + errorMessage: null, + updateSuccess: false, + updating: true, + }); + } + ); + }); + + it('should reset the state', () => { + expect( + reducer( + { ...initialState, loading: true }, + { + type: ACTION_TYPES.RESET, + } + ) + ).toEqual({ + ...initialState, + }); + }); + }); + + describe('Failures', () => { + it('should set a message in errorMessage', () => { + testMultipleTypes( + [ + FAILURE(ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST), + FAILURE(ACTION_TYPES.FETCH_PRODUCTCATEGORY), + FAILURE(ACTION_TYPES.CREATE_PRODUCTCATEGORY), + FAILURE(ACTION_TYPES.UPDATE_PRODUCTCATEGORY), + FAILURE(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTCATEGORY), + FAILURE(ACTION_TYPES.DELETE_PRODUCTCATEGORY), + ], + 'error message', + state => { + expect(state).toMatchObject({ + errorMessage: 'error message', + updateSuccess: false, + updating: false, + }); + } + ); + }); + }); + + describe('Successes', () => { + it('should fetch all entities', () => { + const payload = { data: [{ 1: 'fake1' }, { 2: 'fake2' }], headers: { 'x-total-count': 123 } }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST), + payload, + }) + ).toEqual({ + ...initialState, + loading: false, + totalItems: payload.headers['x-total-count'], + entities: payload.data, + }); + }); + + it('should fetch a single entity', () => { + const payload = { data: { 1: 'fake1' } }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCTCATEGORY), + payload, + }) + ).toEqual({ + ...initialState, + loading: false, + entity: payload.data, + }); + }); + + it('should create/update entity', () => { + const payload = { data: 'fake payload' }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.CREATE_PRODUCTCATEGORY), + payload, + }) + ).toEqual({ + ...initialState, + updating: false, + updateSuccess: true, + entity: payload.data, + }); + }); + + it('should delete entity', () => { + const payload = 'fake payload'; + const toTest = reducer(undefined, { + type: SUCCESS(ACTION_TYPES.DELETE_PRODUCTCATEGORY), + payload, + }); + expect(toTest).toMatchObject({ + updating: false, + updateSuccess: true, + }); + }); + }); + + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + beforeEach(() => { + const mockStore = configureStore([thunk, promiseMiddleware]); + store = mockStore({}); + axios.get = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.post = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.put = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.patch = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.delete = sinon.stub().returns(Promise.resolve(resolvedObject)); + }); + + it('dispatches ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(getEntities()).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.FETCH_PRODUCTCATEGORY actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_PRODUCTCATEGORY), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCTCATEGORY), + payload: resolvedObject, + }, + ]; + await store.dispatch(getEntity(42666)).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.CREATE_PRODUCTCATEGORY actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.CREATE_PRODUCTCATEGORY), + }, + { + type: SUCCESS(ACTION_TYPES.CREATE_PRODUCTCATEGORY), + payload: resolvedObject, + }, + { + type: REQUEST(ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(createEntity({ id: 456 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.UPDATE_PRODUCTCATEGORY actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.UPDATE_PRODUCTCATEGORY), + }, + { + type: SUCCESS(ACTION_TYPES.UPDATE_PRODUCTCATEGORY), + payload: resolvedObject, + }, + ]; + await store.dispatch(updateEntity({ id: 456 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.PARTIAL_UPDATE_PRODUCTCATEGORY actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTCATEGORY), + }, + { + type: SUCCESS(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTCATEGORY), + payload: resolvedObject, + }, + ]; + await store.dispatch(partialUpdate({ id: 1 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.DELETE_PRODUCTCATEGORY actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.DELETE_PRODUCTCATEGORY), + }, + { + type: SUCCESS(ACTION_TYPES.DELETE_PRODUCTCATEGORY), + payload: resolvedObject, + }, + { + type: REQUEST(ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(deleteEntity(42666)).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.RESET actions', async () => { + const expectedActions = [ + { + type: ACTION_TYPES.RESET, + }, + ]; + await store.dispatch(reset()); + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/src/main/webapp/app/entities/product-category/product-category-update.tsx b/src/main/webapp/app/entities/product-category/product-category-update.tsx new file mode 100644 index 0000000..1829cbe --- /dev/null +++ b/src/main/webapp/app/entities/product-category/product-category-update.tsx @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Row, Col, Label } from 'reactstrap'; +import { AvFeedback, AvForm, AvGroup, AvInput, AvField } from 'availity-reactstrap-validation'; +import { translate } from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IRootState } from 'app/shared/reducers'; + +import { getEntity, updateEntity, createEntity, reset } from './product-category.reducer'; +import { IProductCategory } from 'app/shared/model/product-category.model'; +import { convertDateTimeFromServer, convertDateTimeToServer, displayDefaultDateTime } from 'app/shared/util/date-utils'; +import { mapIdList } from 'app/shared/util/entity-utils'; + +export interface IProductCategoryUpdateProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const ProductCategoryUpdate = (props: IProductCategoryUpdateProps) => { + const [isNew] = useState(!props.match.params || !props.match.params.id); + + const { productCategoryEntity, loading, updating } = props; + + const handleClose = () => { + props.history.push('/product-category' + props.location.search); + }; + + useEffect(() => { + if (isNew) { + props.reset(); + } else { + props.getEntity(props.match.params.id); + } + }, []); + + useEffect(() => { + if (props.updateSuccess) { + handleClose(); + } + }, [props.updateSuccess]); + + const saveEntity = (event, errors, values) => { + if (errors.length === 0) { + const entity = { + ...productCategoryEntity, + ...values, + }; + + if (isNew) { + props.createEntity(entity); + } else { + props.updateEntity(entity); + } + } + }; + + return ( +
+ + +

+ Create or edit a ProductCategory +

+ +
+ + + {loading ? ( +

Loading...

+ ) : ( + + {!isNew ? ( + + + + + ) : null} + + + + + + + + + +   + + + )} + +
+
+ ); +}; + +const mapStateToProps = (storeState: IRootState) => ({ + productCategoryEntity: storeState.productCategory.entity, + loading: storeState.productCategory.loading, + updating: storeState.productCategory.updating, + updateSuccess: storeState.productCategory.updateSuccess, +}); + +const mapDispatchToProps = { + getEntity, + updateEntity, + createEntity, + reset, +}; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ProductCategoryUpdate); diff --git a/src/main/webapp/app/entities/product-category/product-category.reducer.ts b/src/main/webapp/app/entities/product-category/product-category.reducer.ts new file mode 100644 index 0000000..795cb92 --- /dev/null +++ b/src/main/webapp/app/entities/product-category/product-category.reducer.ts @@ -0,0 +1,161 @@ +import axios from 'axios'; +import { ICrudGetAction, ICrudGetAllAction, ICrudPutAction, ICrudDeleteAction } from 'react-jhipster'; + +import { cleanEntity } from 'app/shared/util/entity-utils'; +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; + +import { IProductCategory, defaultValue } from 'app/shared/model/product-category.model'; + +export const ACTION_TYPES = { + FETCH_PRODUCTCATEGORY_LIST: 'productCategory/FETCH_PRODUCTCATEGORY_LIST', + FETCH_PRODUCTCATEGORY: 'productCategory/FETCH_PRODUCTCATEGORY', + CREATE_PRODUCTCATEGORY: 'productCategory/CREATE_PRODUCTCATEGORY', + UPDATE_PRODUCTCATEGORY: 'productCategory/UPDATE_PRODUCTCATEGORY', + PARTIAL_UPDATE_PRODUCTCATEGORY: 'productCategory/PARTIAL_UPDATE_PRODUCTCATEGORY', + DELETE_PRODUCTCATEGORY: 'productCategory/DELETE_PRODUCTCATEGORY', + RESET: 'productCategory/RESET', +}; + +const initialState = { + loading: false, + errorMessage: null, + entities: [] as ReadonlyArray, + entity: defaultValue, + updating: false, + totalItems: 0, + updateSuccess: false, +}; + +export type ProductCategoryState = Readonly; + +// Reducer + +export default (state: ProductCategoryState = initialState, action): ProductCategoryState => { + switch (action.type) { + case REQUEST(ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST): + case REQUEST(ACTION_TYPES.FETCH_PRODUCTCATEGORY): + return { + ...state, + errorMessage: null, + updateSuccess: false, + loading: true, + }; + case REQUEST(ACTION_TYPES.CREATE_PRODUCTCATEGORY): + case REQUEST(ACTION_TYPES.UPDATE_PRODUCTCATEGORY): + case REQUEST(ACTION_TYPES.DELETE_PRODUCTCATEGORY): + case REQUEST(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTCATEGORY): + return { + ...state, + errorMessage: null, + updateSuccess: false, + updating: true, + }; + case FAILURE(ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST): + case FAILURE(ACTION_TYPES.FETCH_PRODUCTCATEGORY): + case FAILURE(ACTION_TYPES.CREATE_PRODUCTCATEGORY): + case FAILURE(ACTION_TYPES.UPDATE_PRODUCTCATEGORY): + case FAILURE(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTCATEGORY): + case FAILURE(ACTION_TYPES.DELETE_PRODUCTCATEGORY): + return { + ...state, + loading: false, + updating: false, + updateSuccess: false, + errorMessage: action.payload, + }; + case SUCCESS(ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST): + return { + ...state, + loading: false, + entities: action.payload.data, + totalItems: parseInt(action.payload.headers['x-total-count'], 10), + }; + case SUCCESS(ACTION_TYPES.FETCH_PRODUCTCATEGORY): + return { + ...state, + loading: false, + entity: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.CREATE_PRODUCTCATEGORY): + case SUCCESS(ACTION_TYPES.UPDATE_PRODUCTCATEGORY): + case SUCCESS(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTCATEGORY): + return { + ...state, + updating: false, + updateSuccess: true, + entity: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.DELETE_PRODUCTCATEGORY): + return { + ...state, + updating: false, + updateSuccess: true, + entity: {}, + }; + case ACTION_TYPES.RESET: + return { + ...initialState, + }; + default: + return state; + } +}; + +const apiUrl = 'api/product-categories'; + +// Actions + +export const getEntities: ICrudGetAllAction = (page, size, sort) => { + const requestUrl = `${apiUrl}${sort ? `?page=${page}&size=${size}&sort=${sort}` : ''}`; + return { + type: ACTION_TYPES.FETCH_PRODUCTCATEGORY_LIST, + payload: axios.get(`${requestUrl}${sort ? '&' : '?'}cacheBuster=${new Date().getTime()}`), + }; +}; + +export const getEntity: ICrudGetAction = id => { + const requestUrl = `${apiUrl}/${id}`; + return { + type: ACTION_TYPES.FETCH_PRODUCTCATEGORY, + payload: axios.get(requestUrl), + }; +}; + +export const createEntity: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.CREATE_PRODUCTCATEGORY, + payload: axios.post(apiUrl, cleanEntity(entity)), + }); + dispatch(getEntities()); + return result; +}; + +export const updateEntity: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.UPDATE_PRODUCTCATEGORY, + payload: axios.put(`${apiUrl}/${entity.id}`, cleanEntity(entity)), + }); + return result; +}; + +export const partialUpdate: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.PARTIAL_UPDATE_PRODUCTCATEGORY, + payload: axios.patch(`${apiUrl}/${entity.id}`, cleanEntity(entity)), + }); + return result; +}; + +export const deleteEntity: ICrudDeleteAction = id => async dispatch => { + const requestUrl = `${apiUrl}/${id}`; + const result = await dispatch({ + type: ACTION_TYPES.DELETE_PRODUCTCATEGORY, + payload: axios.delete(requestUrl), + }); + dispatch(getEntities()); + return result; +}; + +export const reset = () => ({ + type: ACTION_TYPES.RESET, +}); diff --git a/src/main/webapp/app/entities/product-category/product-category.tsx b/src/main/webapp/app/entities/product-category/product-category.tsx new file mode 100644 index 0000000..be330fc --- /dev/null +++ b/src/main/webapp/app/entities/product-category/product-category.tsx @@ -0,0 +1,182 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Col, Row, Table } from 'reactstrap'; +import { Translate, getSortState, IPaginationBaseState, JhiPagination, JhiItemCount } from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntities } from './product-category.reducer'; +import { IProductCategory } from 'app/shared/model/product-category.model'; +import { APP_DATE_FORMAT, APP_LOCAL_DATE_FORMAT } from 'app/config/constants'; +import { ITEMS_PER_PAGE } from 'app/shared/util/pagination.constants'; +import { overridePaginationStateWithQueryParams } from 'app/shared/util/entity-utils'; + +export interface IProductCategoryProps extends StateProps, DispatchProps, RouteComponentProps<{ url: string }> {} + +export const ProductCategory = (props: IProductCategoryProps) => { + const [paginationState, setPaginationState] = useState( + overridePaginationStateWithQueryParams(getSortState(props.location, ITEMS_PER_PAGE, 'id'), props.location.search) + ); + + const getAllEntities = () => { + props.getEntities(paginationState.activePage - 1, paginationState.itemsPerPage, `${paginationState.sort},${paginationState.order}`); + }; + + const sortEntities = () => { + getAllEntities(); + const endURL = `?page=${paginationState.activePage}&sort=${paginationState.sort},${paginationState.order}`; + if (props.location.search !== endURL) { + props.history.push(`${props.location.pathname}${endURL}`); + } + }; + + useEffect(() => { + sortEntities(); + }, [paginationState.activePage, paginationState.order, paginationState.sort]); + + useEffect(() => { + const params = new URLSearchParams(props.location.search); + const page = params.get('page'); + const sort = params.get('sort'); + if (page && sort) { + const sortSplit = sort.split(','); + setPaginationState({ + ...paginationState, + activePage: +page, + sort: sortSplit[0], + order: sortSplit[1], + }); + } + }, [props.location.search]); + + const sort = p => () => { + setPaginationState({ + ...paginationState, + order: paginationState.order === 'asc' ? 'desc' : 'asc', + sort: p, + }); + }; + + const handlePagination = currentPage => + setPaginationState({ + ...paginationState, + activePage: currentPage, + }); + + const handleSyncList = () => { + sortEntities(); + }; + + const { productCategoryList, match, loading, totalItems } = props; + return ( +
+

+ Product Categories +
+ + + +   Create new Product Category + +
+

+
+ {productCategoryList && productCategoryList.length > 0 ? ( + + + + + + + + + + {productCategoryList.map((productCategory, i) => ( + + + + + + + + ))} + +
+ ID + + Name + + Description + +
+ + {productCategory.id}{productCategory.name}{productCategory.description} +
+ + + +
+
+ ) : ( + !loading &&
No Product Categories found
+ )} +
+ {props.totalItems ? ( +
0 ? '' : 'd-none'}> + + + + + + +
+ ) : ( + '' + )} +
+ ); +}; + +const mapStateToProps = ({ productCategory }: IRootState) => ({ + productCategoryList: productCategory.entities, + loading: productCategory.loading, + totalItems: productCategory.totalItems, +}); + +const mapDispatchToProps = { + getEntities, +}; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ProductCategory); diff --git a/src/main/webapp/app/entities/product-order/index.tsx b/src/main/webapp/app/entities/product-order/index.tsx new file mode 100644 index 0000000..a3b26b2 --- /dev/null +++ b/src/main/webapp/app/entities/product-order/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Switch } from 'react-router-dom'; + +import ErrorBoundaryRoute from 'app/shared/error/error-boundary-route'; + +import ProductOrder from './product-order'; +import ProductOrderDetail from './product-order-detail'; +import ProductOrderUpdate from './product-order-update'; +import ProductOrderDeleteDialog from './product-order-delete-dialog'; + +const Routes = ({ match }) => ( + <> + + + + + + + + +); + +export default Routes; diff --git a/src/main/webapp/app/entities/product-order/product-order-delete-dialog.tsx b/src/main/webapp/app/entities/product-order/product-order-delete-dialog.tsx new file mode 100644 index 0000000..a775866 --- /dev/null +++ b/src/main/webapp/app/entities/product-order/product-order-delete-dialog.tsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { RouteComponentProps } from 'react-router-dom'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntity, deleteEntity } from './product-order.reducer'; + +export interface IProductOrderDeleteDialogProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const ProductOrderDeleteDialog = (props: IProductOrderDeleteDialogProps) => { + useEffect(() => { + props.getEntity(props.match.params.id); + }, []); + + const handleClose = () => { + props.history.push('/product-order'); + }; + + useEffect(() => { + if (props.updateSuccess) { + handleClose(); + } + }, [props.updateSuccess]); + + const confirmDelete = () => { + props.deleteEntity(props.productOrderEntity.id); + }; + + const { productOrderEntity } = props; + return ( + + + Confirm delete operation + + Are you sure you want to delete this ProductOrder? + + + + + + ); +}; + +const mapStateToProps = ({ productOrder }: IRootState) => ({ + productOrderEntity: productOrder.entity, + updateSuccess: productOrder.updateSuccess, +}); + +const mapDispatchToProps = { getEntity, deleteEntity }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ProductOrderDeleteDialog); diff --git a/src/main/webapp/app/entities/product-order/product-order-detail.tsx b/src/main/webapp/app/entities/product-order/product-order-detail.tsx new file mode 100644 index 0000000..57b9460 --- /dev/null +++ b/src/main/webapp/app/entities/product-order/product-order-detail.tsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Row, Col } from 'reactstrap'; +import {} from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntity } from './product-order.reducer'; +import { APP_DATE_FORMAT, APP_LOCAL_DATE_FORMAT } from 'app/config/constants'; + +export interface IProductOrderDetailProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const ProductOrderDetail = (props: IProductOrderDetailProps) => { + useEffect(() => { + props.getEntity(props.match.params.id); + }, []); + + const { productOrderEntity } = props; + return ( + + +

ProductOrder

+
+
+ ID +
+
{productOrderEntity.id}
+
+ Quantity +
+
{productOrderEntity.quantity}
+
+ Total Price +
+
{productOrderEntity.totalPrice}
+
Product
+
{productOrderEntity.product ? productOrderEntity.product.name : ''}
+
Cart
+
{productOrderEntity.cart ? productOrderEntity.cart.id : ''}
+
+ +   + + +
+ ); +}; + +const mapStateToProps = ({ productOrder }: IRootState) => ({ + productOrderEntity: productOrder.entity, +}); + +const mapDispatchToProps = { getEntity }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ProductOrderDetail); diff --git a/src/main/webapp/app/entities/product-order/product-order-reducer.spec.ts b/src/main/webapp/app/entities/product-order/product-order-reducer.spec.ts new file mode 100644 index 0000000..5e49457 --- /dev/null +++ b/src/main/webapp/app/entities/product-order/product-order-reducer.spec.ts @@ -0,0 +1,302 @@ +import axios from 'axios'; + +import configureStore from 'redux-mock-store'; +import promiseMiddleware from 'redux-promise-middleware'; +import thunk from 'redux-thunk'; +import sinon from 'sinon'; + +import reducer, { + ACTION_TYPES, + createEntity, + deleteEntity, + getEntities, + getEntity, + updateEntity, + partialUpdate, + reset, +} from './product-order.reducer'; +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; +import { IProductOrder, defaultValue } from 'app/shared/model/product-order.model'; + +describe('Entities reducer tests', () => { + function isEmpty(element): boolean { + if (element instanceof Array) { + return element.length === 0; + } else { + return Object.keys(element).length === 0; + } + } + + const initialState = { + loading: false, + errorMessage: null, + entities: [] as ReadonlyArray, + entity: defaultValue, + updating: false, + updateSuccess: false, + }; + + function testInitialState(state) { + expect(state).toMatchObject({ + loading: false, + errorMessage: null, + updating: false, + updateSuccess: false, + }); + expect(isEmpty(state.entities)); + expect(isEmpty(state.entity)); + } + + function testMultipleTypes(types, payload, testFunction) { + types.forEach(e => { + testFunction(reducer(undefined, { type: e, payload })); + }); + } + + describe('Common', () => { + it('should return the initial state', () => { + testInitialState(reducer(undefined, {})); + }); + }); + + describe('Requests', () => { + it('should set state to loading', () => { + testMultipleTypes([REQUEST(ACTION_TYPES.FETCH_PRODUCTORDER_LIST), REQUEST(ACTION_TYPES.FETCH_PRODUCTORDER)], {}, state => { + expect(state).toMatchObject({ + errorMessage: null, + updateSuccess: false, + loading: true, + }); + }); + }); + + it('should set state to updating', () => { + testMultipleTypes( + [ + REQUEST(ACTION_TYPES.CREATE_PRODUCTORDER), + REQUEST(ACTION_TYPES.UPDATE_PRODUCTORDER), + REQUEST(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTORDER), + REQUEST(ACTION_TYPES.DELETE_PRODUCTORDER), + ], + {}, + state => { + expect(state).toMatchObject({ + errorMessage: null, + updateSuccess: false, + updating: true, + }); + } + ); + }); + + it('should reset the state', () => { + expect( + reducer( + { ...initialState, loading: true }, + { + type: ACTION_TYPES.RESET, + } + ) + ).toEqual({ + ...initialState, + }); + }); + }); + + describe('Failures', () => { + it('should set a message in errorMessage', () => { + testMultipleTypes( + [ + FAILURE(ACTION_TYPES.FETCH_PRODUCTORDER_LIST), + FAILURE(ACTION_TYPES.FETCH_PRODUCTORDER), + FAILURE(ACTION_TYPES.CREATE_PRODUCTORDER), + FAILURE(ACTION_TYPES.UPDATE_PRODUCTORDER), + FAILURE(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTORDER), + FAILURE(ACTION_TYPES.DELETE_PRODUCTORDER), + ], + 'error message', + state => { + expect(state).toMatchObject({ + errorMessage: 'error message', + updateSuccess: false, + updating: false, + }); + } + ); + }); + }); + + describe('Successes', () => { + it('should fetch all entities', () => { + const payload = { data: [{ 1: 'fake1' }, { 2: 'fake2' }] }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCTORDER_LIST), + payload, + }) + ).toEqual({ + ...initialState, + loading: false, + entities: payload.data, + }); + }); + + it('should fetch a single entity', () => { + const payload = { data: { 1: 'fake1' } }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCTORDER), + payload, + }) + ).toEqual({ + ...initialState, + loading: false, + entity: payload.data, + }); + }); + + it('should create/update entity', () => { + const payload = { data: 'fake payload' }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.CREATE_PRODUCTORDER), + payload, + }) + ).toEqual({ + ...initialState, + updating: false, + updateSuccess: true, + entity: payload.data, + }); + }); + + it('should delete entity', () => { + const payload = 'fake payload'; + const toTest = reducer(undefined, { + type: SUCCESS(ACTION_TYPES.DELETE_PRODUCTORDER), + payload, + }); + expect(toTest).toMatchObject({ + updating: false, + updateSuccess: true, + }); + }); + }); + + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + beforeEach(() => { + const mockStore = configureStore([thunk, promiseMiddleware]); + store = mockStore({}); + axios.get = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.post = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.put = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.patch = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.delete = sinon.stub().returns(Promise.resolve(resolvedObject)); + }); + + it('dispatches ACTION_TYPES.FETCH_PRODUCTORDER_LIST actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_PRODUCTORDER_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCTORDER_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(getEntities()).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.FETCH_PRODUCTORDER actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_PRODUCTORDER), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCTORDER), + payload: resolvedObject, + }, + ]; + await store.dispatch(getEntity(42666)).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.CREATE_PRODUCTORDER actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.CREATE_PRODUCTORDER), + }, + { + type: SUCCESS(ACTION_TYPES.CREATE_PRODUCTORDER), + payload: resolvedObject, + }, + { + type: REQUEST(ACTION_TYPES.FETCH_PRODUCTORDER_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCTORDER_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(createEntity({ id: 456 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.UPDATE_PRODUCTORDER actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.UPDATE_PRODUCTORDER), + }, + { + type: SUCCESS(ACTION_TYPES.UPDATE_PRODUCTORDER), + payload: resolvedObject, + }, + ]; + await store.dispatch(updateEntity({ id: 456 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.PARTIAL_UPDATE_PRODUCTORDER actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTORDER), + }, + { + type: SUCCESS(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTORDER), + payload: resolvedObject, + }, + ]; + await store.dispatch(partialUpdate({ id: 1 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.DELETE_PRODUCTORDER actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.DELETE_PRODUCTORDER), + }, + { + type: SUCCESS(ACTION_TYPES.DELETE_PRODUCTORDER), + payload: resolvedObject, + }, + { + type: REQUEST(ACTION_TYPES.FETCH_PRODUCTORDER_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCTORDER_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(deleteEntity(42666)).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.RESET actions', async () => { + const expectedActions = [ + { + type: ACTION_TYPES.RESET, + }, + ]; + await store.dispatch(reset()); + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/src/main/webapp/app/entities/product-order/product-order-update.tsx b/src/main/webapp/app/entities/product-order/product-order-update.tsx new file mode 100644 index 0000000..9228a71 --- /dev/null +++ b/src/main/webapp/app/entities/product-order/product-order-update.tsx @@ -0,0 +1,185 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Row, Col, Label } from 'reactstrap'; +import { AvFeedback, AvForm, AvGroup, AvInput, AvField } from 'availity-reactstrap-validation'; +import { translate } from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IRootState } from 'app/shared/reducers'; + +import { IProduct } from 'app/shared/model/product.model'; +import { getEntities as getProducts } from 'app/entities/product/product.reducer'; +import { IShoppingCart } from 'app/shared/model/shopping-cart.model'; +import { getEntities as getShoppingCarts } from 'app/entities/shopping-cart/shopping-cart.reducer'; +import { getEntity, updateEntity, createEntity, reset } from './product-order.reducer'; +import { IProductOrder } from 'app/shared/model/product-order.model'; +import { convertDateTimeFromServer, convertDateTimeToServer, displayDefaultDateTime } from 'app/shared/util/date-utils'; +import { mapIdList } from 'app/shared/util/entity-utils'; + +export interface IProductOrderUpdateProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const ProductOrderUpdate = (props: IProductOrderUpdateProps) => { + const [isNew] = useState(!props.match.params || !props.match.params.id); + + const { productOrderEntity, products, shoppingCarts, loading, updating } = props; + + const handleClose = () => { + props.history.push('/product-order'); + }; + + useEffect(() => { + if (isNew) { + props.reset(); + } else { + props.getEntity(props.match.params.id); + } + + props.getProducts(); + props.getShoppingCarts(); + }, []); + + useEffect(() => { + if (props.updateSuccess) { + handleClose(); + } + }, [props.updateSuccess]); + + const saveEntity = (event, errors, values) => { + if (errors.length === 0) { + const entity = { + ...productOrderEntity, + ...values, + product: products.find(it => it.id.toString() === values.productId.toString()), + cart: shoppingCarts.find(it => it.id.toString() === values.cartId.toString()), + }; + + if (isNew) { + props.createEntity(entity); + } else { + props.updateEntity(entity); + } + } + }; + + return ( +
+ + +

+ Create or edit a ProductOrder +

+ +
+ + + {loading ? ( +

Loading...

+ ) : ( + + {!isNew ? ( + + + + + ) : null} + + + + + + + + + + + + + )) + : null} + + This field is required. + + + + + + )) + : null} + + This field is required. + + +   + + + )} + +
+
+ ); +}; + +const mapStateToProps = (storeState: IRootState) => ({ + products: storeState.product.entities, + shoppingCarts: storeState.shoppingCart.entities, + productOrderEntity: storeState.productOrder.entity, + loading: storeState.productOrder.loading, + updating: storeState.productOrder.updating, + updateSuccess: storeState.productOrder.updateSuccess, +}); + +const mapDispatchToProps = { + getProducts, + getShoppingCarts, + getEntity, + updateEntity, + createEntity, + reset, +}; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ProductOrderUpdate); diff --git a/src/main/webapp/app/entities/product-order/product-order.reducer.ts b/src/main/webapp/app/entities/product-order/product-order.reducer.ts new file mode 100644 index 0000000..bac2f68 --- /dev/null +++ b/src/main/webapp/app/entities/product-order/product-order.reducer.ts @@ -0,0 +1,156 @@ +import axios from 'axios'; +import { ICrudGetAction, ICrudGetAllAction, ICrudPutAction, ICrudDeleteAction } from 'react-jhipster'; + +import { cleanEntity } from 'app/shared/util/entity-utils'; +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; + +import { IProductOrder, defaultValue } from 'app/shared/model/product-order.model'; + +export const ACTION_TYPES = { + FETCH_PRODUCTORDER_LIST: 'productOrder/FETCH_PRODUCTORDER_LIST', + FETCH_PRODUCTORDER: 'productOrder/FETCH_PRODUCTORDER', + CREATE_PRODUCTORDER: 'productOrder/CREATE_PRODUCTORDER', + UPDATE_PRODUCTORDER: 'productOrder/UPDATE_PRODUCTORDER', + PARTIAL_UPDATE_PRODUCTORDER: 'productOrder/PARTIAL_UPDATE_PRODUCTORDER', + DELETE_PRODUCTORDER: 'productOrder/DELETE_PRODUCTORDER', + RESET: 'productOrder/RESET', +}; + +const initialState = { + loading: false, + errorMessage: null, + entities: [] as ReadonlyArray, + entity: defaultValue, + updating: false, + updateSuccess: false, +}; + +export type ProductOrderState = Readonly; + +// Reducer + +export default (state: ProductOrderState = initialState, action): ProductOrderState => { + switch (action.type) { + case REQUEST(ACTION_TYPES.FETCH_PRODUCTORDER_LIST): + case REQUEST(ACTION_TYPES.FETCH_PRODUCTORDER): + return { + ...state, + errorMessage: null, + updateSuccess: false, + loading: true, + }; + case REQUEST(ACTION_TYPES.CREATE_PRODUCTORDER): + case REQUEST(ACTION_TYPES.UPDATE_PRODUCTORDER): + case REQUEST(ACTION_TYPES.DELETE_PRODUCTORDER): + case REQUEST(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTORDER): + return { + ...state, + errorMessage: null, + updateSuccess: false, + updating: true, + }; + case FAILURE(ACTION_TYPES.FETCH_PRODUCTORDER_LIST): + case FAILURE(ACTION_TYPES.FETCH_PRODUCTORDER): + case FAILURE(ACTION_TYPES.CREATE_PRODUCTORDER): + case FAILURE(ACTION_TYPES.UPDATE_PRODUCTORDER): + case FAILURE(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTORDER): + case FAILURE(ACTION_TYPES.DELETE_PRODUCTORDER): + return { + ...state, + loading: false, + updating: false, + updateSuccess: false, + errorMessage: action.payload, + }; + case SUCCESS(ACTION_TYPES.FETCH_PRODUCTORDER_LIST): + return { + ...state, + loading: false, + entities: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.FETCH_PRODUCTORDER): + return { + ...state, + loading: false, + entity: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.CREATE_PRODUCTORDER): + case SUCCESS(ACTION_TYPES.UPDATE_PRODUCTORDER): + case SUCCESS(ACTION_TYPES.PARTIAL_UPDATE_PRODUCTORDER): + return { + ...state, + updating: false, + updateSuccess: true, + entity: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.DELETE_PRODUCTORDER): + return { + ...state, + updating: false, + updateSuccess: true, + entity: {}, + }; + case ACTION_TYPES.RESET: + return { + ...initialState, + }; + default: + return state; + } +}; + +const apiUrl = 'api/product-orders'; + +// Actions + +export const getEntities: ICrudGetAllAction = (page, size, sort) => ({ + type: ACTION_TYPES.FETCH_PRODUCTORDER_LIST, + payload: axios.get(`${apiUrl}?cacheBuster=${new Date().getTime()}`), +}); + +export const getEntity: ICrudGetAction = id => { + const requestUrl = `${apiUrl}/${id}`; + return { + type: ACTION_TYPES.FETCH_PRODUCTORDER, + payload: axios.get(requestUrl), + }; +}; + +export const createEntity: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.CREATE_PRODUCTORDER, + payload: axios.post(apiUrl, cleanEntity(entity)), + }); + dispatch(getEntities()); + return result; +}; + +export const updateEntity: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.UPDATE_PRODUCTORDER, + payload: axios.put(`${apiUrl}/${entity.id}`, cleanEntity(entity)), + }); + return result; +}; + +export const partialUpdate: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.PARTIAL_UPDATE_PRODUCTORDER, + payload: axios.patch(`${apiUrl}/${entity.id}`, cleanEntity(entity)), + }); + return result; +}; + +export const deleteEntity: ICrudDeleteAction = id => async dispatch => { + const requestUrl = `${apiUrl}/${id}`; + const result = await dispatch({ + type: ACTION_TYPES.DELETE_PRODUCTORDER, + payload: axios.delete(requestUrl), + }); + dispatch(getEntities()); + return result; +}; + +export const reset = () => ({ + type: ACTION_TYPES.RESET, +}); diff --git a/src/main/webapp/app/entities/product-order/product-order.tsx b/src/main/webapp/app/entities/product-order/product-order.tsx new file mode 100644 index 0000000..3e6705f --- /dev/null +++ b/src/main/webapp/app/entities/product-order/product-order.tsx @@ -0,0 +1,108 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Col, Row, Table } from 'reactstrap'; +import { Translate } from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntities } from './product-order.reducer'; +import { IProductOrder } from 'app/shared/model/product-order.model'; +import { APP_DATE_FORMAT, APP_LOCAL_DATE_FORMAT } from 'app/config/constants'; + +export interface IProductOrderProps extends StateProps, DispatchProps, RouteComponentProps<{ url: string }> {} + +export const ProductOrder = (props: IProductOrderProps) => { + useEffect(() => { + props.getEntities(); + }, []); + + const handleSyncList = () => { + props.getEntities(); + }; + + const { productOrderList, match, loading } = props; + return ( +
+

+ Product Orders +
+ + + +   Create new Product Order + +
+

+
+ {productOrderList && productOrderList.length > 0 ? ( + + + + + + + + + + + + {productOrderList.map((productOrder, i) => ( + + + + + + + + + + ))} + +
IDQuantityTotal PriceProductCart +
+ + {productOrder.id}{productOrder.quantity}{productOrder.totalPrice}{productOrder.product ? {productOrder.product.name} : ''}{productOrder.cart ? {productOrder.cart.id} : ''} +
+ + + +
+
+ ) : ( + !loading &&
No Product Orders found
+ )} +
+
+ ); +}; + +const mapStateToProps = ({ productOrder }: IRootState) => ({ + productOrderList: productOrder.entities, + loading: productOrder.loading, +}); + +const mapDispatchToProps = { + getEntities, +}; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ProductOrder); diff --git a/src/main/webapp/app/entities/product/index.tsx b/src/main/webapp/app/entities/product/index.tsx new file mode 100644 index 0000000..b8dd75f --- /dev/null +++ b/src/main/webapp/app/entities/product/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Switch } from 'react-router-dom'; + +import ErrorBoundaryRoute from 'app/shared/error/error-boundary-route'; + +import Product from './product'; +import ProductDetail from './product-detail'; +import ProductUpdate from './product-update'; +import ProductDeleteDialog from './product-delete-dialog'; + +const Routes = ({ match }) => ( + <> + + + + + + + + +); + +export default Routes; diff --git a/src/main/webapp/app/entities/product/product-delete-dialog.tsx b/src/main/webapp/app/entities/product/product-delete-dialog.tsx new file mode 100644 index 0000000..87eb6ec --- /dev/null +++ b/src/main/webapp/app/entities/product/product-delete-dialog.tsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { RouteComponentProps } from 'react-router-dom'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntity, deleteEntity } from './product.reducer'; + +export interface IProductDeleteDialogProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const ProductDeleteDialog = (props: IProductDeleteDialogProps) => { + useEffect(() => { + props.getEntity(props.match.params.id); + }, []); + + const handleClose = () => { + props.history.push('/product' + props.location.search); + }; + + useEffect(() => { + if (props.updateSuccess) { + handleClose(); + } + }, [props.updateSuccess]); + + const confirmDelete = () => { + props.deleteEntity(props.productEntity.id); + }; + + const { productEntity } = props; + return ( + + + Confirm delete operation + + Are you sure you want to delete this Product? + + + + + + ); +}; + +const mapStateToProps = ({ product }: IRootState) => ({ + productEntity: product.entity, + updateSuccess: product.updateSuccess, +}); + +const mapDispatchToProps = { getEntity, deleteEntity }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ProductDeleteDialog); diff --git a/src/main/webapp/app/entities/product/product-detail.tsx b/src/main/webapp/app/entities/product/product-detail.tsx new file mode 100644 index 0000000..e4d889a --- /dev/null +++ b/src/main/webapp/app/entities/product/product-detail.tsx @@ -0,0 +1,86 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Row, Col } from 'reactstrap'; +import { openFile, byteSize } from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntity } from './product.reducer'; +import { APP_DATE_FORMAT, APP_LOCAL_DATE_FORMAT } from 'app/config/constants'; + +export interface IProductDetailProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const ProductDetail = (props: IProductDetailProps) => { + useEffect(() => { + props.getEntity(props.match.params.id); + }, []); + + const { productEntity } = props; + return ( + + +

Product

+
+
+ ID +
+
{productEntity.id}
+
+ Name +
+
{productEntity.name}
+
+ Description +
+
{productEntity.description}
+
+ Price +
+
{productEntity.price}
+
+ Item Size +
+
{productEntity.itemSize}
+
+ Image +
+
+ {productEntity.image ? ( +
+ {productEntity.imageContentType ? ( + + + + ) : null} + + {productEntity.imageContentType}, {byteSize(productEntity.image)} + +
+ ) : null} +
+
Product Category
+
{productEntity.productCategory ? productEntity.productCategory.name : ''}
+
+ +   + + +
+ ); +}; + +const mapStateToProps = ({ product }: IRootState) => ({ + productEntity: product.entity, +}); + +const mapDispatchToProps = { getEntity }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ProductDetail); diff --git a/src/main/webapp/app/entities/product/product-reducer.spec.ts b/src/main/webapp/app/entities/product/product-reducer.spec.ts new file mode 100644 index 0000000..5d290d2 --- /dev/null +++ b/src/main/webapp/app/entities/product/product-reducer.spec.ts @@ -0,0 +1,323 @@ +import axios from 'axios'; + +import configureStore from 'redux-mock-store'; +import promiseMiddleware from 'redux-promise-middleware'; +import thunk from 'redux-thunk'; +import sinon from 'sinon'; + +import reducer, { + ACTION_TYPES, + createEntity, + deleteEntity, + getEntities, + getEntity, + updateEntity, + partialUpdate, + reset, +} from './product.reducer'; +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; +import { IProduct, defaultValue } from 'app/shared/model/product.model'; + +describe('Entities reducer tests', () => { + function isEmpty(element): boolean { + if (element instanceof Array) { + return element.length === 0; + } else { + return Object.keys(element).length === 0; + } + } + + const initialState = { + loading: false, + errorMessage: null, + entities: [] as ReadonlyArray, + entity: defaultValue, + totalItems: 0, + updating: false, + updateSuccess: false, + }; + + function testInitialState(state) { + expect(state).toMatchObject({ + loading: false, + errorMessage: null, + updating: false, + updateSuccess: false, + }); + expect(isEmpty(state.entities)); + expect(isEmpty(state.entity)); + } + + function testMultipleTypes(types, payload, testFunction) { + types.forEach(e => { + testFunction(reducer(undefined, { type: e, payload })); + }); + } + + describe('Common', () => { + it('should return the initial state', () => { + testInitialState(reducer(undefined, {})); + }); + }); + + describe('Requests', () => { + it('should set state to loading', () => { + testMultipleTypes([REQUEST(ACTION_TYPES.FETCH_PRODUCT_LIST), REQUEST(ACTION_TYPES.FETCH_PRODUCT)], {}, state => { + expect(state).toMatchObject({ + errorMessage: null, + updateSuccess: false, + loading: true, + }); + }); + }); + + it('should set state to updating', () => { + testMultipleTypes( + [ + REQUEST(ACTION_TYPES.CREATE_PRODUCT), + REQUEST(ACTION_TYPES.UPDATE_PRODUCT), + REQUEST(ACTION_TYPES.PARTIAL_UPDATE_PRODUCT), + REQUEST(ACTION_TYPES.DELETE_PRODUCT), + ], + {}, + state => { + expect(state).toMatchObject({ + errorMessage: null, + updateSuccess: false, + updating: true, + }); + } + ); + }); + + it('should reset the state', () => { + expect( + reducer( + { ...initialState, loading: true }, + { + type: ACTION_TYPES.RESET, + } + ) + ).toEqual({ + ...initialState, + }); + }); + }); + + describe('Failures', () => { + it('should set a message in errorMessage', () => { + testMultipleTypes( + [ + FAILURE(ACTION_TYPES.FETCH_PRODUCT_LIST), + FAILURE(ACTION_TYPES.FETCH_PRODUCT), + FAILURE(ACTION_TYPES.CREATE_PRODUCT), + FAILURE(ACTION_TYPES.UPDATE_PRODUCT), + FAILURE(ACTION_TYPES.PARTIAL_UPDATE_PRODUCT), + FAILURE(ACTION_TYPES.DELETE_PRODUCT), + ], + 'error message', + state => { + expect(state).toMatchObject({ + errorMessage: 'error message', + updateSuccess: false, + updating: false, + }); + } + ); + }); + }); + + describe('Successes', () => { + it('should fetch all entities', () => { + const payload = { data: [{ 1: 'fake1' }, { 2: 'fake2' }], headers: { 'x-total-count': 123 } }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCT_LIST), + payload, + }) + ).toEqual({ + ...initialState, + loading: false, + totalItems: payload.headers['x-total-count'], + entities: payload.data, + }); + }); + + it('should fetch a single entity', () => { + const payload = { data: { 1: 'fake1' } }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCT), + payload, + }) + ).toEqual({ + ...initialState, + loading: false, + entity: payload.data, + }); + }); + + it('should create/update entity', () => { + const payload = { data: 'fake payload' }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.CREATE_PRODUCT), + payload, + }) + ).toEqual({ + ...initialState, + updating: false, + updateSuccess: true, + entity: payload.data, + }); + }); + + it('should delete entity', () => { + const payload = 'fake payload'; + const toTest = reducer(undefined, { + type: SUCCESS(ACTION_TYPES.DELETE_PRODUCT), + payload, + }); + expect(toTest).toMatchObject({ + updating: false, + updateSuccess: true, + }); + }); + }); + + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + beforeEach(() => { + const mockStore = configureStore([thunk, promiseMiddleware]); + store = mockStore({}); + axios.get = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.post = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.put = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.patch = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.delete = sinon.stub().returns(Promise.resolve(resolvedObject)); + }); + + it('dispatches ACTION_TYPES.FETCH_PRODUCT_LIST actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_PRODUCT_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCT_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(getEntities()).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.FETCH_PRODUCT actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_PRODUCT), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCT), + payload: resolvedObject, + }, + ]; + await store.dispatch(getEntity(42666)).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.CREATE_PRODUCT actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.CREATE_PRODUCT), + }, + { + type: SUCCESS(ACTION_TYPES.CREATE_PRODUCT), + payload: resolvedObject, + }, + { + type: REQUEST(ACTION_TYPES.FETCH_PRODUCT_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCT_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(createEntity({ id: 456 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.UPDATE_PRODUCT actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.UPDATE_PRODUCT), + }, + { + type: SUCCESS(ACTION_TYPES.UPDATE_PRODUCT), + payload: resolvedObject, + }, + ]; + await store.dispatch(updateEntity({ id: 456 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.PARTIAL_UPDATE_PRODUCT actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.PARTIAL_UPDATE_PRODUCT), + }, + { + type: SUCCESS(ACTION_TYPES.PARTIAL_UPDATE_PRODUCT), + payload: resolvedObject, + }, + ]; + await store.dispatch(partialUpdate({ id: 1 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.DELETE_PRODUCT actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.DELETE_PRODUCT), + }, + { + type: SUCCESS(ACTION_TYPES.DELETE_PRODUCT), + payload: resolvedObject, + }, + { + type: REQUEST(ACTION_TYPES.FETCH_PRODUCT_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_PRODUCT_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(deleteEntity(42666)).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.RESET actions', async () => { + const expectedActions = [ + { + type: ACTION_TYPES.RESET, + }, + ]; + await store.dispatch(reset()); + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + describe('blobFields', () => { + it('should properly set a blob in state.', () => { + const payload = { name: 'fancyBlobName', data: 'fake data', contentType: 'fake dataType' }; + expect( + reducer(undefined, { + type: ACTION_TYPES.SET_BLOB, + payload, + }) + ).toEqual({ + ...initialState, + entity: { + ...initialState.entity, + fancyBlobName: payload.data, + fancyBlobNameContentType: payload.contentType, + }, + }); + }); + }); +}); diff --git a/src/main/webapp/app/entities/product/product-update.tsx b/src/main/webapp/app/entities/product/product-update.tsx new file mode 100644 index 0000000..93679cc --- /dev/null +++ b/src/main/webapp/app/entities/product/product-update.tsx @@ -0,0 +1,237 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Row, Col, Label } from 'reactstrap'; +import { AvFeedback, AvForm, AvGroup, AvInput, AvField } from 'availity-reactstrap-validation'; +import { setFileData, openFile, byteSize, translate } from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IRootState } from 'app/shared/reducers'; + +import { IProductCategory } from 'app/shared/model/product-category.model'; +import { getEntities as getProductCategories } from 'app/entities/product-category/product-category.reducer'; +import { getEntity, updateEntity, createEntity, setBlob, reset } from './product.reducer'; +import { IProduct } from 'app/shared/model/product.model'; +import { convertDateTimeFromServer, convertDateTimeToServer, displayDefaultDateTime } from 'app/shared/util/date-utils'; +import { mapIdList } from 'app/shared/util/entity-utils'; + +export interface IProductUpdateProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const ProductUpdate = (props: IProductUpdateProps) => { + const [isNew] = useState(!props.match.params || !props.match.params.id); + + const { productEntity, productCategories, loading, updating } = props; + + const { image, imageContentType } = productEntity; + + const handleClose = () => { + props.history.push('/product' + props.location.search); + }; + + useEffect(() => { + if (isNew) { + props.reset(); + } else { + props.getEntity(props.match.params.id); + } + + props.getProductCategories(); + }, []); + + const onBlobChange = (isAnImage, name) => event => { + setFileData(event, (contentType, data) => props.setBlob(name, data, contentType), isAnImage); + }; + + const clearBlob = name => () => { + props.setBlob(name, undefined, undefined); + }; + + useEffect(() => { + if (props.updateSuccess) { + handleClose(); + } + }, [props.updateSuccess]); + + const saveEntity = (event, errors, values) => { + if (errors.length === 0) { + const entity = { + ...productEntity, + ...values, + productCategory: productCategories.find(it => it.id.toString() === values.productCategoryId.toString()), + }; + + if (isNew) { + props.createEntity(entity); + } else { + props.updateEntity(entity); + } + } + }; + + return ( +
+ + +

+ Create or edit a Product +

+ +
+ + + {loading ? ( +

Loading...

+ ) : ( + + {!isNew ? ( + + + + + ) : null} + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {image ? ( +
+ {imageContentType ? ( + + + + ) : null} +
+ + + + {imageContentType}, {byteSize(image)} + + + + + + +
+ ) : null} + + +
+
+ + + + + )) + : null} + + This field is required. + + +   + +
+ )} + +
+
+ ); +}; + +const mapStateToProps = (storeState: IRootState) => ({ + productCategories: storeState.productCategory.entities, + productEntity: storeState.product.entity, + loading: storeState.product.loading, + updating: storeState.product.updating, + updateSuccess: storeState.product.updateSuccess, +}); + +const mapDispatchToProps = { + getProductCategories, + getEntity, + updateEntity, + setBlob, + createEntity, + reset, +}; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ProductUpdate); diff --git a/src/main/webapp/app/entities/product/product.reducer.ts b/src/main/webapp/app/entities/product/product.reducer.ts new file mode 100644 index 0000000..c143f98 --- /dev/null +++ b/src/main/webapp/app/entities/product/product.reducer.ts @@ -0,0 +1,182 @@ +import axios from 'axios'; +import { ICrudGetAction, ICrudGetAllAction, ICrudPutAction, ICrudDeleteAction } from 'react-jhipster'; + +import { cleanEntity } from 'app/shared/util/entity-utils'; +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; + +import { IProduct, defaultValue } from 'app/shared/model/product.model'; + +export const ACTION_TYPES = { + FETCH_PRODUCT_LIST: 'product/FETCH_PRODUCT_LIST', + FETCH_PRODUCT: 'product/FETCH_PRODUCT', + CREATE_PRODUCT: 'product/CREATE_PRODUCT', + UPDATE_PRODUCT: 'product/UPDATE_PRODUCT', + PARTIAL_UPDATE_PRODUCT: 'product/PARTIAL_UPDATE_PRODUCT', + DELETE_PRODUCT: 'product/DELETE_PRODUCT', + SET_BLOB: 'product/SET_BLOB', + RESET: 'product/RESET', +}; + +const initialState = { + loading: false, + errorMessage: null, + entities: [] as ReadonlyArray, + entity: defaultValue, + updating: false, + totalItems: 0, + updateSuccess: false, +}; + +export type ProductState = Readonly; + +// Reducer + +export default (state: ProductState = initialState, action): ProductState => { + switch (action.type) { + case REQUEST(ACTION_TYPES.FETCH_PRODUCT_LIST): + case REQUEST(ACTION_TYPES.FETCH_PRODUCT): + return { + ...state, + errorMessage: null, + updateSuccess: false, + loading: true, + }; + case REQUEST(ACTION_TYPES.CREATE_PRODUCT): + case REQUEST(ACTION_TYPES.UPDATE_PRODUCT): + case REQUEST(ACTION_TYPES.DELETE_PRODUCT): + case REQUEST(ACTION_TYPES.PARTIAL_UPDATE_PRODUCT): + return { + ...state, + errorMessage: null, + updateSuccess: false, + updating: true, + }; + case FAILURE(ACTION_TYPES.FETCH_PRODUCT_LIST): + case FAILURE(ACTION_TYPES.FETCH_PRODUCT): + case FAILURE(ACTION_TYPES.CREATE_PRODUCT): + case FAILURE(ACTION_TYPES.UPDATE_PRODUCT): + case FAILURE(ACTION_TYPES.PARTIAL_UPDATE_PRODUCT): + case FAILURE(ACTION_TYPES.DELETE_PRODUCT): + return { + ...state, + loading: false, + updating: false, + updateSuccess: false, + errorMessage: action.payload, + }; + case SUCCESS(ACTION_TYPES.FETCH_PRODUCT_LIST): + return { + ...state, + loading: false, + entities: action.payload.data, + totalItems: parseInt(action.payload.headers['x-total-count'], 10), + }; + case SUCCESS(ACTION_TYPES.FETCH_PRODUCT): + return { + ...state, + loading: false, + entity: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.CREATE_PRODUCT): + case SUCCESS(ACTION_TYPES.UPDATE_PRODUCT): + case SUCCESS(ACTION_TYPES.PARTIAL_UPDATE_PRODUCT): + return { + ...state, + updating: false, + updateSuccess: true, + entity: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.DELETE_PRODUCT): + return { + ...state, + updating: false, + updateSuccess: true, + entity: {}, + }; + case ACTION_TYPES.SET_BLOB: { + const { name, data, contentType } = action.payload; + return { + ...state, + entity: { + ...state.entity, + [name]: data, + [name + 'ContentType']: contentType, + }, + }; + } + case ACTION_TYPES.RESET: + return { + ...initialState, + }; + default: + return state; + } +}; + +const apiUrl = 'api/products'; + +// Actions + +export const getEntities: ICrudGetAllAction = (page, size, sort) => { + const requestUrl = `${apiUrl}${sort ? `?page=${page}&size=${size}&sort=${sort}` : ''}`; + return { + type: ACTION_TYPES.FETCH_PRODUCT_LIST, + payload: axios.get(`${requestUrl}${sort ? '&' : '?'}cacheBuster=${new Date().getTime()}`), + }; +}; + +export const getEntity: ICrudGetAction = id => { + const requestUrl = `${apiUrl}/${id}`; + return { + type: ACTION_TYPES.FETCH_PRODUCT, + payload: axios.get(requestUrl), + }; +}; + +export const createEntity: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.CREATE_PRODUCT, + payload: axios.post(apiUrl, cleanEntity(entity)), + }); + dispatch(getEntities()); + return result; +}; + +export const updateEntity: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.UPDATE_PRODUCT, + payload: axios.put(`${apiUrl}/${entity.id}`, cleanEntity(entity)), + }); + return result; +}; + +export const partialUpdate: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.PARTIAL_UPDATE_PRODUCT, + payload: axios.patch(`${apiUrl}/${entity.id}`, cleanEntity(entity)), + }); + return result; +}; + +export const deleteEntity: ICrudDeleteAction = id => async dispatch => { + const requestUrl = `${apiUrl}/${id}`; + const result = await dispatch({ + type: ACTION_TYPES.DELETE_PRODUCT, + payload: axios.delete(requestUrl), + }); + dispatch(getEntities()); + return result; +}; + +export const setBlob = (name, data, contentType?) => ({ + type: ACTION_TYPES.SET_BLOB, + payload: { + name, + data, + contentType, + }, +}); + +export const reset = () => ({ + type: ACTION_TYPES.RESET, +}); diff --git a/src/main/webapp/app/entities/product/product.tsx b/src/main/webapp/app/entities/product/product.tsx new file mode 100644 index 0000000..05d634b --- /dev/null +++ b/src/main/webapp/app/entities/product/product.tsx @@ -0,0 +1,218 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Col, Row, Table } from 'reactstrap'; +import { openFile, byteSize, Translate, getSortState, IPaginationBaseState, JhiPagination, JhiItemCount } from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntities } from './product.reducer'; +import { IProduct } from 'app/shared/model/product.model'; +import { APP_DATE_FORMAT, APP_LOCAL_DATE_FORMAT } from 'app/config/constants'; +import { ITEMS_PER_PAGE } from 'app/shared/util/pagination.constants'; +import { overridePaginationStateWithQueryParams } from 'app/shared/util/entity-utils'; + +export interface IProductProps extends StateProps, DispatchProps, RouteComponentProps<{ url: string }> {} + +export const Product = (props: IProductProps) => { + const [paginationState, setPaginationState] = useState( + overridePaginationStateWithQueryParams(getSortState(props.location, ITEMS_PER_PAGE, 'id'), props.location.search) + ); + + const getAllEntities = () => { + props.getEntities(paginationState.activePage - 1, paginationState.itemsPerPage, `${paginationState.sort},${paginationState.order}`); + }; + + const sortEntities = () => { + getAllEntities(); + const endURL = `?page=${paginationState.activePage}&sort=${paginationState.sort},${paginationState.order}`; + if (props.location.search !== endURL) { + props.history.push(`${props.location.pathname}${endURL}`); + } + }; + + useEffect(() => { + sortEntities(); + }, [paginationState.activePage, paginationState.order, paginationState.sort]); + + useEffect(() => { + const params = new URLSearchParams(props.location.search); + const page = params.get('page'); + const sort = params.get('sort'); + if (page && sort) { + const sortSplit = sort.split(','); + setPaginationState({ + ...paginationState, + activePage: +page, + sort: sortSplit[0], + order: sortSplit[1], + }); + } + }, [props.location.search]); + + const sort = p => () => { + setPaginationState({ + ...paginationState, + order: paginationState.order === 'asc' ? 'desc' : 'asc', + sort: p, + }); + }; + + const handlePagination = currentPage => + setPaginationState({ + ...paginationState, + activePage: currentPage, + }); + + const handleSyncList = () => { + sortEntities(); + }; + + const { productList, match, loading, totalItems } = props; + return ( +
+

+ Products +
+ + + +   Create new Product + +
+

+
+ {productList && productList.length > 0 ? ( + + + + + + + + + + + + + + {productList.map((product, i) => ( + + + + + + + + + + + + ))} + +
+ ID + + Name + + Description + + Price + + Item Size + + Image + + Product Category + +
+ + {product.id}{product.name}{product.description}{product.price}{product.itemSize} + {product.image ? ( +
+ {product.imageContentType ? ( + + +   + + ) : null} + + {product.imageContentType}, {byteSize(product.image)} + +
+ ) : null} +
+ {product.productCategory ? ( + {product.productCategory.name} + ) : ( + '' + )} + +
+ + + +
+
+ ) : ( + !loading &&
No Products found
+ )} +
+ {props.totalItems ? ( +
0 ? '' : 'd-none'}> + + + + + + +
+ ) : ( + '' + )} +
+ ); +}; + +const mapStateToProps = ({ product }: IRootState) => ({ + productList: product.entities, + loading: product.loading, + totalItems: product.totalItems, +}); + +const mapDispatchToProps = { + getEntities, +}; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(Product); diff --git a/src/main/webapp/app/entities/shopping-cart/index.tsx b/src/main/webapp/app/entities/shopping-cart/index.tsx new file mode 100644 index 0000000..9e8ef67 --- /dev/null +++ b/src/main/webapp/app/entities/shopping-cart/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Switch } from 'react-router-dom'; + +import ErrorBoundaryRoute from 'app/shared/error/error-boundary-route'; + +import ShoppingCart from './shopping-cart'; +import ShoppingCartDetail from './shopping-cart-detail'; +import ShoppingCartUpdate from './shopping-cart-update'; +import ShoppingCartDeleteDialog from './shopping-cart-delete-dialog'; + +const Routes = ({ match }) => ( + <> + + + + + + + + +); + +export default Routes; diff --git a/src/main/webapp/app/entities/shopping-cart/shopping-cart-delete-dialog.tsx b/src/main/webapp/app/entities/shopping-cart/shopping-cart-delete-dialog.tsx new file mode 100644 index 0000000..7b466be --- /dev/null +++ b/src/main/webapp/app/entities/shopping-cart/shopping-cart-delete-dialog.tsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { RouteComponentProps } from 'react-router-dom'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; + +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntity, deleteEntity } from './shopping-cart.reducer'; + +export interface IShoppingCartDeleteDialogProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const ShoppingCartDeleteDialog = (props: IShoppingCartDeleteDialogProps) => { + useEffect(() => { + props.getEntity(props.match.params.id); + }, []); + + const handleClose = () => { + props.history.push('/shopping-cart'); + }; + + useEffect(() => { + if (props.updateSuccess) { + handleClose(); + } + }, [props.updateSuccess]); + + const confirmDelete = () => { + props.deleteEntity(props.shoppingCartEntity.id); + }; + + const { shoppingCartEntity } = props; + return ( + + + Confirm delete operation + + Are you sure you want to delete this ShoppingCart? + + + + + + ); +}; + +const mapStateToProps = ({ shoppingCart }: IRootState) => ({ + shoppingCartEntity: shoppingCart.entity, + updateSuccess: shoppingCart.updateSuccess, +}); + +const mapDispatchToProps = { getEntity, deleteEntity }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ShoppingCartDeleteDialog); diff --git a/src/main/webapp/app/entities/shopping-cart/shopping-cart-detail.tsx b/src/main/webapp/app/entities/shopping-cart/shopping-cart-detail.tsx new file mode 100644 index 0000000..ffc1f48 --- /dev/null +++ b/src/main/webapp/app/entities/shopping-cart/shopping-cart-detail.tsx @@ -0,0 +1,81 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Row, Col } from 'reactstrap'; +import { TextFormat } from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntity } from './shopping-cart.reducer'; +import { APP_DATE_FORMAT, APP_LOCAL_DATE_FORMAT } from 'app/config/constants'; + +export interface IShoppingCartDetailProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const ShoppingCartDetail = (props: IShoppingCartDetailProps) => { + useEffect(() => { + props.getEntity(props.match.params.id); + }, []); + + const { shoppingCartEntity } = props; + return ( + + +

ShoppingCart

+
+
+ ID +
+
{shoppingCartEntity.id}
+
+ Placed Date +
+
+ {shoppingCartEntity.placedDate ? ( + + ) : null} +
+
+ Status +
+
{shoppingCartEntity.status}
+
+ Total Price +
+
{shoppingCartEntity.totalPrice}
+
+ Payment Method +
+
{shoppingCartEntity.paymentMethod}
+
+ Payment Reference +
+
{shoppingCartEntity.paymentReference}
+
+ Payment Modification Reference +
+
{shoppingCartEntity.paymentModificationReference}
+
Customer Details
+
{shoppingCartEntity.customerDetails ? shoppingCartEntity.customerDetails.id : ''}
+
+ +   + + +
+ ); +}; + +const mapStateToProps = ({ shoppingCart }: IRootState) => ({ + shoppingCartEntity: shoppingCart.entity, +}); + +const mapDispatchToProps = { getEntity }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ShoppingCartDetail); diff --git a/src/main/webapp/app/entities/shopping-cart/shopping-cart-reducer.spec.ts b/src/main/webapp/app/entities/shopping-cart/shopping-cart-reducer.spec.ts new file mode 100644 index 0000000..c50170e --- /dev/null +++ b/src/main/webapp/app/entities/shopping-cart/shopping-cart-reducer.spec.ts @@ -0,0 +1,302 @@ +import axios from 'axios'; + +import configureStore from 'redux-mock-store'; +import promiseMiddleware from 'redux-promise-middleware'; +import thunk from 'redux-thunk'; +import sinon from 'sinon'; + +import reducer, { + ACTION_TYPES, + createEntity, + deleteEntity, + getEntities, + getEntity, + updateEntity, + partialUpdate, + reset, +} from './shopping-cart.reducer'; +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; +import { IShoppingCart, defaultValue } from 'app/shared/model/shopping-cart.model'; + +describe('Entities reducer tests', () => { + function isEmpty(element): boolean { + if (element instanceof Array) { + return element.length === 0; + } else { + return Object.keys(element).length === 0; + } + } + + const initialState = { + loading: false, + errorMessage: null, + entities: [] as ReadonlyArray, + entity: defaultValue, + updating: false, + updateSuccess: false, + }; + + function testInitialState(state) { + expect(state).toMatchObject({ + loading: false, + errorMessage: null, + updating: false, + updateSuccess: false, + }); + expect(isEmpty(state.entities)); + expect(isEmpty(state.entity)); + } + + function testMultipleTypes(types, payload, testFunction) { + types.forEach(e => { + testFunction(reducer(undefined, { type: e, payload })); + }); + } + + describe('Common', () => { + it('should return the initial state', () => { + testInitialState(reducer(undefined, {})); + }); + }); + + describe('Requests', () => { + it('should set state to loading', () => { + testMultipleTypes([REQUEST(ACTION_TYPES.FETCH_SHOPPINGCART_LIST), REQUEST(ACTION_TYPES.FETCH_SHOPPINGCART)], {}, state => { + expect(state).toMatchObject({ + errorMessage: null, + updateSuccess: false, + loading: true, + }); + }); + }); + + it('should set state to updating', () => { + testMultipleTypes( + [ + REQUEST(ACTION_TYPES.CREATE_SHOPPINGCART), + REQUEST(ACTION_TYPES.UPDATE_SHOPPINGCART), + REQUEST(ACTION_TYPES.PARTIAL_UPDATE_SHOPPINGCART), + REQUEST(ACTION_TYPES.DELETE_SHOPPINGCART), + ], + {}, + state => { + expect(state).toMatchObject({ + errorMessage: null, + updateSuccess: false, + updating: true, + }); + } + ); + }); + + it('should reset the state', () => { + expect( + reducer( + { ...initialState, loading: true }, + { + type: ACTION_TYPES.RESET, + } + ) + ).toEqual({ + ...initialState, + }); + }); + }); + + describe('Failures', () => { + it('should set a message in errorMessage', () => { + testMultipleTypes( + [ + FAILURE(ACTION_TYPES.FETCH_SHOPPINGCART_LIST), + FAILURE(ACTION_TYPES.FETCH_SHOPPINGCART), + FAILURE(ACTION_TYPES.CREATE_SHOPPINGCART), + FAILURE(ACTION_TYPES.UPDATE_SHOPPINGCART), + FAILURE(ACTION_TYPES.PARTIAL_UPDATE_SHOPPINGCART), + FAILURE(ACTION_TYPES.DELETE_SHOPPINGCART), + ], + 'error message', + state => { + expect(state).toMatchObject({ + errorMessage: 'error message', + updateSuccess: false, + updating: false, + }); + } + ); + }); + }); + + describe('Successes', () => { + it('should fetch all entities', () => { + const payload = { data: [{ 1: 'fake1' }, { 2: 'fake2' }] }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.FETCH_SHOPPINGCART_LIST), + payload, + }) + ).toEqual({ + ...initialState, + loading: false, + entities: payload.data, + }); + }); + + it('should fetch a single entity', () => { + const payload = { data: { 1: 'fake1' } }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.FETCH_SHOPPINGCART), + payload, + }) + ).toEqual({ + ...initialState, + loading: false, + entity: payload.data, + }); + }); + + it('should create/update entity', () => { + const payload = { data: 'fake payload' }; + expect( + reducer(undefined, { + type: SUCCESS(ACTION_TYPES.CREATE_SHOPPINGCART), + payload, + }) + ).toEqual({ + ...initialState, + updating: false, + updateSuccess: true, + entity: payload.data, + }); + }); + + it('should delete entity', () => { + const payload = 'fake payload'; + const toTest = reducer(undefined, { + type: SUCCESS(ACTION_TYPES.DELETE_SHOPPINGCART), + payload, + }); + expect(toTest).toMatchObject({ + updating: false, + updateSuccess: true, + }); + }); + }); + + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + beforeEach(() => { + const mockStore = configureStore([thunk, promiseMiddleware]); + store = mockStore({}); + axios.get = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.post = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.put = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.patch = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.delete = sinon.stub().returns(Promise.resolve(resolvedObject)); + }); + + it('dispatches ACTION_TYPES.FETCH_SHOPPINGCART_LIST actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_SHOPPINGCART_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_SHOPPINGCART_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(getEntities()).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.FETCH_SHOPPINGCART actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_SHOPPINGCART), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_SHOPPINGCART), + payload: resolvedObject, + }, + ]; + await store.dispatch(getEntity(42666)).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.CREATE_SHOPPINGCART actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.CREATE_SHOPPINGCART), + }, + { + type: SUCCESS(ACTION_TYPES.CREATE_SHOPPINGCART), + payload: resolvedObject, + }, + { + type: REQUEST(ACTION_TYPES.FETCH_SHOPPINGCART_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_SHOPPINGCART_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(createEntity({ id: 456 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.UPDATE_SHOPPINGCART actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.UPDATE_SHOPPINGCART), + }, + { + type: SUCCESS(ACTION_TYPES.UPDATE_SHOPPINGCART), + payload: resolvedObject, + }, + ]; + await store.dispatch(updateEntity({ id: 456 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.PARTIAL_UPDATE_SHOPPINGCART actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.PARTIAL_UPDATE_SHOPPINGCART), + }, + { + type: SUCCESS(ACTION_TYPES.PARTIAL_UPDATE_SHOPPINGCART), + payload: resolvedObject, + }, + ]; + await store.dispatch(partialUpdate({ id: 1 })).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.DELETE_SHOPPINGCART actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.DELETE_SHOPPINGCART), + }, + { + type: SUCCESS(ACTION_TYPES.DELETE_SHOPPINGCART), + payload: resolvedObject, + }, + { + type: REQUEST(ACTION_TYPES.FETCH_SHOPPINGCART_LIST), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_SHOPPINGCART_LIST), + payload: resolvedObject, + }, + ]; + await store.dispatch(deleteEntity(42666)).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + + it('dispatches ACTION_TYPES.RESET actions', async () => { + const expectedActions = [ + { + type: ACTION_TYPES.RESET, + }, + ]; + await store.dispatch(reset()); + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/src/main/webapp/app/entities/shopping-cart/shopping-cart-update.tsx b/src/main/webapp/app/entities/shopping-cart/shopping-cart-update.tsx new file mode 100644 index 0000000..c1afc13 --- /dev/null +++ b/src/main/webapp/app/entities/shopping-cart/shopping-cart-update.tsx @@ -0,0 +1,228 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Row, Col, Label } from 'reactstrap'; +import { AvFeedback, AvForm, AvGroup, AvInput, AvField } from 'availity-reactstrap-validation'; +import { translate } from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IRootState } from 'app/shared/reducers'; + +import { ICustomerDetails } from 'app/shared/model/customer-details.model'; +import { getEntities as getCustomerDetails } from 'app/entities/customer-details/customer-details.reducer'; +import { getEntity, updateEntity, createEntity, reset } from './shopping-cart.reducer'; +import { IShoppingCart } from 'app/shared/model/shopping-cart.model'; +import { convertDateTimeFromServer, convertDateTimeToServer, displayDefaultDateTime } from 'app/shared/util/date-utils'; +import { mapIdList } from 'app/shared/util/entity-utils'; + +export interface IShoppingCartUpdateProps extends StateProps, DispatchProps, RouteComponentProps<{ id: string }> {} + +export const ShoppingCartUpdate = (props: IShoppingCartUpdateProps) => { + const [isNew] = useState(!props.match.params || !props.match.params.id); + + const { shoppingCartEntity, customerDetails, loading, updating } = props; + + const handleClose = () => { + props.history.push('/shopping-cart'); + }; + + useEffect(() => { + if (isNew) { + props.reset(); + } else { + props.getEntity(props.match.params.id); + } + + props.getCustomerDetails(); + }, []); + + useEffect(() => { + if (props.updateSuccess) { + handleClose(); + } + }, [props.updateSuccess]); + + const saveEntity = (event, errors, values) => { + values.placedDate = convertDateTimeToServer(values.placedDate); + + if (errors.length === 0) { + const entity = { + ...shoppingCartEntity, + ...values, + customerDetails: customerDetails.find(it => it.id.toString() === values.customerDetailsId.toString()), + }; + + if (isNew) { + props.createEntity(entity); + } else { + props.updateEntity(entity); + } + } + }; + + return ( +
+ + +

+ Create or edit a ShoppingCart +

+ +
+ + + {loading ? ( +

Loading...

+ ) : ( + + {!isNew ? ( + + + + + ) : null} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )) + : null} + + This field is required. + + +   + + + )} + +
+
+ ); +}; + +const mapStateToProps = (storeState: IRootState) => ({ + customerDetails: storeState.customerDetails.entities, + shoppingCartEntity: storeState.shoppingCart.entity, + loading: storeState.shoppingCart.loading, + updating: storeState.shoppingCart.updating, + updateSuccess: storeState.shoppingCart.updateSuccess, +}); + +const mapDispatchToProps = { + getCustomerDetails, + getEntity, + updateEntity, + createEntity, + reset, +}; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ShoppingCartUpdate); diff --git a/src/main/webapp/app/entities/shopping-cart/shopping-cart.reducer.ts b/src/main/webapp/app/entities/shopping-cart/shopping-cart.reducer.ts new file mode 100644 index 0000000..fa8e285 --- /dev/null +++ b/src/main/webapp/app/entities/shopping-cart/shopping-cart.reducer.ts @@ -0,0 +1,156 @@ +import axios from 'axios'; +import { ICrudGetAction, ICrudGetAllAction, ICrudPutAction, ICrudDeleteAction } from 'react-jhipster'; + +import { cleanEntity } from 'app/shared/util/entity-utils'; +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; + +import { IShoppingCart, defaultValue } from 'app/shared/model/shopping-cart.model'; + +export const ACTION_TYPES = { + FETCH_SHOPPINGCART_LIST: 'shoppingCart/FETCH_SHOPPINGCART_LIST', + FETCH_SHOPPINGCART: 'shoppingCart/FETCH_SHOPPINGCART', + CREATE_SHOPPINGCART: 'shoppingCart/CREATE_SHOPPINGCART', + UPDATE_SHOPPINGCART: 'shoppingCart/UPDATE_SHOPPINGCART', + PARTIAL_UPDATE_SHOPPINGCART: 'shoppingCart/PARTIAL_UPDATE_SHOPPINGCART', + DELETE_SHOPPINGCART: 'shoppingCart/DELETE_SHOPPINGCART', + RESET: 'shoppingCart/RESET', +}; + +const initialState = { + loading: false, + errorMessage: null, + entities: [] as ReadonlyArray, + entity: defaultValue, + updating: false, + updateSuccess: false, +}; + +export type ShoppingCartState = Readonly; + +// Reducer + +export default (state: ShoppingCartState = initialState, action): ShoppingCartState => { + switch (action.type) { + case REQUEST(ACTION_TYPES.FETCH_SHOPPINGCART_LIST): + case REQUEST(ACTION_TYPES.FETCH_SHOPPINGCART): + return { + ...state, + errorMessage: null, + updateSuccess: false, + loading: true, + }; + case REQUEST(ACTION_TYPES.CREATE_SHOPPINGCART): + case REQUEST(ACTION_TYPES.UPDATE_SHOPPINGCART): + case REQUEST(ACTION_TYPES.DELETE_SHOPPINGCART): + case REQUEST(ACTION_TYPES.PARTIAL_UPDATE_SHOPPINGCART): + return { + ...state, + errorMessage: null, + updateSuccess: false, + updating: true, + }; + case FAILURE(ACTION_TYPES.FETCH_SHOPPINGCART_LIST): + case FAILURE(ACTION_TYPES.FETCH_SHOPPINGCART): + case FAILURE(ACTION_TYPES.CREATE_SHOPPINGCART): + case FAILURE(ACTION_TYPES.UPDATE_SHOPPINGCART): + case FAILURE(ACTION_TYPES.PARTIAL_UPDATE_SHOPPINGCART): + case FAILURE(ACTION_TYPES.DELETE_SHOPPINGCART): + return { + ...state, + loading: false, + updating: false, + updateSuccess: false, + errorMessage: action.payload, + }; + case SUCCESS(ACTION_TYPES.FETCH_SHOPPINGCART_LIST): + return { + ...state, + loading: false, + entities: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.FETCH_SHOPPINGCART): + return { + ...state, + loading: false, + entity: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.CREATE_SHOPPINGCART): + case SUCCESS(ACTION_TYPES.UPDATE_SHOPPINGCART): + case SUCCESS(ACTION_TYPES.PARTIAL_UPDATE_SHOPPINGCART): + return { + ...state, + updating: false, + updateSuccess: true, + entity: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.DELETE_SHOPPINGCART): + return { + ...state, + updating: false, + updateSuccess: true, + entity: {}, + }; + case ACTION_TYPES.RESET: + return { + ...initialState, + }; + default: + return state; + } +}; + +const apiUrl = 'api/shopping-carts'; + +// Actions + +export const getEntities: ICrudGetAllAction = (page, size, sort) => ({ + type: ACTION_TYPES.FETCH_SHOPPINGCART_LIST, + payload: axios.get(`${apiUrl}?cacheBuster=${new Date().getTime()}`), +}); + +export const getEntity: ICrudGetAction = id => { + const requestUrl = `${apiUrl}/${id}`; + return { + type: ACTION_TYPES.FETCH_SHOPPINGCART, + payload: axios.get(requestUrl), + }; +}; + +export const createEntity: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.CREATE_SHOPPINGCART, + payload: axios.post(apiUrl, cleanEntity(entity)), + }); + dispatch(getEntities()); + return result; +}; + +export const updateEntity: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.UPDATE_SHOPPINGCART, + payload: axios.put(`${apiUrl}/${entity.id}`, cleanEntity(entity)), + }); + return result; +}; + +export const partialUpdate: ICrudPutAction = entity => async dispatch => { + const result = await dispatch({ + type: ACTION_TYPES.PARTIAL_UPDATE_SHOPPINGCART, + payload: axios.patch(`${apiUrl}/${entity.id}`, cleanEntity(entity)), + }); + return result; +}; + +export const deleteEntity: ICrudDeleteAction = id => async dispatch => { + const requestUrl = `${apiUrl}/${id}`; + const result = await dispatch({ + type: ACTION_TYPES.DELETE_SHOPPINGCART, + payload: axios.delete(requestUrl), + }); + dispatch(getEntities()); + return result; +}; + +export const reset = () => ({ + type: ACTION_TYPES.RESET, +}); diff --git a/src/main/webapp/app/entities/shopping-cart/shopping-cart.tsx b/src/main/webapp/app/entities/shopping-cart/shopping-cart.tsx new file mode 100644 index 0000000..b2fe067 --- /dev/null +++ b/src/main/webapp/app/entities/shopping-cart/shopping-cart.tsx @@ -0,0 +1,122 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Button, Col, Row, Table } from 'reactstrap'; +import { Translate, TextFormat } from 'react-jhipster'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; + +import { IRootState } from 'app/shared/reducers'; +import { getEntities } from './shopping-cart.reducer'; +import { IShoppingCart } from 'app/shared/model/shopping-cart.model'; +import { APP_DATE_FORMAT, APP_LOCAL_DATE_FORMAT } from 'app/config/constants'; + +export interface IShoppingCartProps extends StateProps, DispatchProps, RouteComponentProps<{ url: string }> {} + +export const ShoppingCart = (props: IShoppingCartProps) => { + useEffect(() => { + props.getEntities(); + }, []); + + const handleSyncList = () => { + props.getEntities(); + }; + + const { shoppingCartList, match, loading } = props; + return ( +
+

+ Shopping Carts +
+ + + +   Create new Shopping Cart + +
+

+
+ {shoppingCartList && shoppingCartList.length > 0 ? ( + + + + + + + + + + + + + + + {shoppingCartList.map((shoppingCart, i) => ( + + + + + + + + + + + + + ))} + +
IDPlaced DateStatusTotal PricePayment MethodPayment ReferencePayment Modification ReferenceCustomer Details +
+ + {shoppingCart.id} + {shoppingCart.placedDate ? : null} + {shoppingCart.status}{shoppingCart.totalPrice}{shoppingCart.paymentMethod}{shoppingCart.paymentReference}{shoppingCart.paymentModificationReference} + {shoppingCart.customerDetails ? ( + {shoppingCart.customerDetails.id} + ) : ( + '' + )} + +
+ + + +
+
+ ) : ( + !loading &&
No Shopping Carts found
+ )} +
+
+ ); +}; + +const mapStateToProps = ({ shoppingCart }: IRootState) => ({ + shoppingCartList: shoppingCart.entities, + loading: shoppingCart.loading, +}); + +const mapDispatchToProps = { + getEntities, +}; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ShoppingCart); diff --git a/src/main/webapp/app/index.tsx b/src/main/webapp/app/index.tsx new file mode 100644 index 0000000..4982786 --- /dev/null +++ b/src/main/webapp/app/index.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import DevTools from './config/devtools'; +import initStore from './config/store'; +import setupAxiosInterceptors from './config/axios-interceptor'; +import { clearAuthentication } from './shared/reducers/authentication'; +import ErrorBoundary from './shared/error/error-boundary'; +import AppComponent from './app'; +import { loadIcons } from './config/icon-loader'; + +const devTools = process.env.NODE_ENV === 'development' ? : null; + +const store = initStore(); + +const actions = bindActionCreators({ clearAuthentication }, store.dispatch); +setupAxiosInterceptors(() => actions.clearAuthentication('login.error.unauthorized')); + +loadIcons(); + +const rootEl = document.getElementById('root'); + +const render = Component => + // eslint-disable-next-line react/no-render-return-value + ReactDOM.render( + + +
+ {/* If this slows down the app in dev disable it and enable when required */} + {devTools} + +
+
+
, + rootEl + ); + +render(AppComponent); diff --git a/src/main/webapp/app/modules/account/activate/activate.reducer.spec.ts b/src/main/webapp/app/modules/account/activate/activate.reducer.spec.ts new file mode 100644 index 0000000..46e3406 --- /dev/null +++ b/src/main/webapp/app/modules/account/activate/activate.reducer.spec.ts @@ -0,0 +1,95 @@ +import thunk from 'redux-thunk'; +import axios from 'axios'; +import sinon from 'sinon'; +import configureStore from 'redux-mock-store'; +import promiseMiddleware from 'redux-promise-middleware'; + +import { SUCCESS, FAILURE, REQUEST } from 'app/shared/reducers/action-type.util'; +import activate, { ACTION_TYPES, activateAction, reset } from './activate.reducer'; + +describe('Activate reducer tests', () => { + it('should return the initial state', () => { + expect(activate(undefined, {})).toMatchObject({ + activationSuccess: false, + activationFailure: false, + }); + }); + + it('should reset', () => { + expect(activate({ activationSuccess: true, activationFailure: false }, { type: ACTION_TYPES.RESET })).toMatchObject({ + activationSuccess: false, + activationFailure: false, + }); + }); + + it('should detect a success', () => { + expect(activate(undefined, { type: SUCCESS(ACTION_TYPES.ACTIVATE_ACCOUNT) })).toMatchObject({ + activationSuccess: true, + activationFailure: false, + }); + }); + + it('should return the same state on request', () => { + expect(activate(undefined, { type: REQUEST(ACTION_TYPES.ACTIVATE_ACCOUNT) })).toMatchObject({ + activationSuccess: false, + activationFailure: false, + }); + }); + + it('should detect a failure', () => { + expect(activate(undefined, { type: FAILURE(ACTION_TYPES.ACTIVATE_ACCOUNT) })).toMatchObject({ + activationSuccess: false, + activationFailure: true, + }); + }); + + it('should reset the state', () => { + const initialState = { + activationSuccess: false, + activationFailure: false, + }; + expect( + activate( + { activationSuccess: true, activationFailure: true }, + { + type: ACTION_TYPES.RESET, + } + ) + ).toEqual({ + ...initialState, + }); + }); + + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + beforeEach(() => { + const mockStore = configureStore([thunk, promiseMiddleware]); + store = mockStore({}); + axios.get = sinon.stub().returns(Promise.resolve(resolvedObject)); + }); + + it('dispatches ACTIVATE_ACCOUNT_PENDING and ACTIVATE_ACCOUNT_FULFILLED actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.ACTIVATE_ACCOUNT), + }, + { + type: SUCCESS(ACTION_TYPES.ACTIVATE_ACCOUNT), + payload: resolvedObject, + }, + ]; + await store.dispatch(activateAction('')).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + it('dispatches ACTION_TYPES.RESET actions', async () => { + const expectedActions = [ + { + type: ACTION_TYPES.RESET, + }, + ]; + await store.dispatch(reset()); + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/src/main/webapp/app/modules/account/activate/activate.reducer.ts b/src/main/webapp/app/modules/account/activate/activate.reducer.ts new file mode 100644 index 0000000..bb419ac --- /dev/null +++ b/src/main/webapp/app/modules/account/activate/activate.reducer.ts @@ -0,0 +1,51 @@ +import axios from 'axios'; + +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; + +export const ACTION_TYPES = { + ACTIVATE_ACCOUNT: 'activate/ACTIVATE_ACCOUNT', + RESET: 'activate/RESET', +}; + +const initialState = { + activationSuccess: false, + activationFailure: false, +}; + +export type ActivateState = Readonly; + +// Reducer +export default (state: ActivateState = initialState, action): ActivateState => { + switch (action.type) { + case REQUEST(ACTION_TYPES.ACTIVATE_ACCOUNT): + return { + ...state, + }; + case FAILURE(ACTION_TYPES.ACTIVATE_ACCOUNT): + return { + ...state, + activationFailure: true, + }; + case SUCCESS(ACTION_TYPES.ACTIVATE_ACCOUNT): + return { + ...state, + activationSuccess: true, + }; + case ACTION_TYPES.RESET: + return { + ...initialState, + }; + default: + return state; + } +}; + +// Actions +export const activateAction = key => ({ + type: ACTION_TYPES.ACTIVATE_ACCOUNT, + payload: axios.get('api/activate?key=' + key), +}); + +export const reset = () => ({ + type: ACTION_TYPES.RESET, +}); diff --git a/src/main/webapp/app/modules/account/activate/activate.tsx b/src/main/webapp/app/modules/account/activate/activate.tsx new file mode 100644 index 0000000..ee08350 --- /dev/null +++ b/src/main/webapp/app/modules/account/activate/activate.tsx @@ -0,0 +1,60 @@ +import React, { useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Link, RouteComponentProps } from 'react-router-dom'; +import { Row, Col, Alert } from 'reactstrap'; +import { getUrlParameter } from 'react-jhipster'; + +import { IRootState } from 'app/shared/reducers'; +import { activateAction, reset } from './activate.reducer'; + +const successAlert = ( + + Your user account has been activated. Please + + sign in + + . + +); + +const failureAlert = ( + + Your user could not be activated. Please use the registration form to sign up. + +); + +export interface IActivateProps extends StateProps, DispatchProps, RouteComponentProps<{ key: any }> {} + +export const ActivatePage = (props: IActivateProps) => { + useEffect(() => { + const key = getUrlParameter('key', props.location.search); + props.activateAction(key); + return () => { + props.reset(); + }; + }, []); + + return ( +
+ + +

Activation

+ {props.activationSuccess ? successAlert : undefined} + {props.activationFailure ? failureAlert : undefined} + +
+
+ ); +}; + +const mapStateToProps = ({ activate }: IRootState) => ({ + activationSuccess: activate.activationSuccess, + activationFailure: activate.activationFailure, +}); + +const mapDispatchToProps = { activateAction, reset }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ActivatePage); diff --git a/src/main/webapp/app/modules/account/index.tsx b/src/main/webapp/app/modules/account/index.tsx new file mode 100644 index 0000000..4874460 --- /dev/null +++ b/src/main/webapp/app/modules/account/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import ErrorBoundaryRoute from 'app/shared/error/error-boundary-route'; + +import Settings from './settings/settings'; +import Password from './password/password'; + +const Routes = ({ match }) => ( +
+ + +
+); + +export default Routes; diff --git a/src/main/webapp/app/modules/account/password-reset/finish/password-reset-finish.tsx b/src/main/webapp/app/modules/account/password-reset/finish/password-reset-finish.tsx new file mode 100644 index 0000000..28cf4af --- /dev/null +++ b/src/main/webapp/app/modules/account/password-reset/finish/password-reset-finish.tsx @@ -0,0 +1,81 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Col, Row, Button } from 'reactstrap'; +import { AvForm, AvField } from 'availity-reactstrap-validation'; +import { getUrlParameter } from 'react-jhipster'; +import { RouteComponentProps } from 'react-router-dom'; + +import { handlePasswordResetFinish, reset } from '../password-reset.reducer'; +import PasswordStrengthBar from 'app/shared/layout/password/password-strength-bar'; + +export interface IPasswordResetFinishProps extends DispatchProps, RouteComponentProps<{ key: string }> {} + +export const PasswordResetFinishPage = (props: IPasswordResetFinishProps) => { + const [password, setPassword] = useState(''); + const [key] = useState(getUrlParameter('key', props.location.search)); + + useEffect( + () => () => { + props.reset(); + }, + [] + ); + + const handleValidSubmit = (event, values) => props.handlePasswordResetFinish(key, values.newPassword); + + const updatePassword = event => setPassword(event.target.value); + + const getResetForm = () => { + return ( + + + + + + + ); + }; + + return ( +
+ + +

Reset password

+
{key ? getResetForm() : null}
+ +
+
+ ); +}; + +const mapDispatchToProps = { handlePasswordResetFinish, reset }; + +type DispatchProps = typeof mapDispatchToProps; + +export default connect(null, mapDispatchToProps)(PasswordResetFinishPage); diff --git a/src/main/webapp/app/modules/account/password-reset/init/password-reset-init.tsx b/src/main/webapp/app/modules/account/password-reset/init/password-reset-init.tsx new file mode 100644 index 0000000..f1a4f90 --- /dev/null +++ b/src/main/webapp/app/modules/account/password-reset/init/password-reset-init.tsx @@ -0,0 +1,58 @@ +import React from 'react'; + +import { connect } from 'react-redux'; +import { AvForm, AvField } from 'availity-reactstrap-validation'; +import { Button, Alert, Col, Row } from 'reactstrap'; + +import { handlePasswordResetInit, reset } from '../password-reset.reducer'; + +export type IPasswordResetInitProps = DispatchProps; + +export class PasswordResetInit extends React.Component { + componentWillUnmount() { + this.props.reset(); + } + + handleValidSubmit = (event, values) => { + this.props.handlePasswordResetInit(values.email); + event.preventDefault(); + }; + + render() { + return ( +
+ + +

Reset your password

+ +

Enter the email address you used to register

+
+ + + + + +
+
+ ); + } +} + +const mapDispatchToProps = { handlePasswordResetInit, reset }; + +type DispatchProps = typeof mapDispatchToProps; + +export default connect(null, mapDispatchToProps)(PasswordResetInit); diff --git a/src/main/webapp/app/modules/account/password-reset/password-reset.reducer.ts b/src/main/webapp/app/modules/account/password-reset/password-reset.reducer.ts new file mode 100644 index 0000000..0129a40 --- /dev/null +++ b/src/main/webapp/app/modules/account/password-reset/password-reset.reducer.ts @@ -0,0 +1,73 @@ +import axios from 'axios'; + +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; + +export const ACTION_TYPES = { + RESET_PASSWORD_INIT: 'passwordReset/RESET_PASSWORD_INIT', + RESET_PASSWORD_FINISH: 'passwordReset/RESET_PASSWORD_FINISH', + RESET: 'passwordReset/RESET', +}; + +const initialState = { + loading: false, + resetPasswordSuccess: false, + resetPasswordFailure: false, +}; + +export type PasswordResetState = Readonly; + +// Reducer +export default (state: PasswordResetState = initialState, action): PasswordResetState => { + switch (action.type) { + case REQUEST(ACTION_TYPES.RESET_PASSWORD_FINISH): + case REQUEST(ACTION_TYPES.RESET_PASSWORD_INIT): + return { + ...state, + loading: true, + }; + case FAILURE(ACTION_TYPES.RESET_PASSWORD_FINISH): + case FAILURE(ACTION_TYPES.RESET_PASSWORD_INIT): + return { + ...initialState, + loading: false, + resetPasswordFailure: true, + }; + case SUCCESS(ACTION_TYPES.RESET_PASSWORD_FINISH): + case SUCCESS(ACTION_TYPES.RESET_PASSWORD_INIT): + return { + ...initialState, + loading: false, + resetPasswordSuccess: true, + }; + case ACTION_TYPES.RESET: + return { + ...initialState, + }; + default: + return state; + } +}; + +const apiUrl = 'api/account/reset-password'; + +// Actions +export const handlePasswordResetInit = mail => ({ + type: ACTION_TYPES.RESET_PASSWORD_INIT, + // If the content-type isn't set that way, axios will try to encode the body and thus modify the data sent to the server. + payload: axios.post(`${apiUrl}/init`, mail, { headers: { ['Content-Type']: 'text/plain' } }), + meta: { + successMessage: 'Check your emails for details on how to reset your password.', + }, +}); + +export const handlePasswordResetFinish = (key, newPassword) => ({ + type: ACTION_TYPES.RESET_PASSWORD_FINISH, + payload: axios.post(`${apiUrl}/finish`, { key, newPassword }), + meta: { + successMessage: 'Your password has been reset. Please ', + }, +}); + +export const reset = () => ({ + type: ACTION_TYPES.RESET, +}); diff --git a/src/main/webapp/app/modules/account/password/password.reducer.spec.ts b/src/main/webapp/app/modules/account/password/password.reducer.spec.ts new file mode 100644 index 0000000..08f5e50 --- /dev/null +++ b/src/main/webapp/app/modules/account/password/password.reducer.spec.ts @@ -0,0 +1,108 @@ +import thunk from 'redux-thunk'; +import axios from 'axios'; +import sinon from 'sinon'; +import configureStore from 'redux-mock-store'; +import promiseMiddleware from 'redux-promise-middleware'; + +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; +import password, { ACTION_TYPES, savePassword, reset } from './password.reducer'; + +describe('Password reducer tests', () => { + describe('Common tests', () => { + it('should return the initial state', () => { + const toTest = password(undefined, {}); + expect(toTest).toMatchObject({ + loading: false, + errorMessage: null, + updateSuccess: false, + updateFailure: false, + }); + }); + }); + + describe('Password update', () => { + it('should detect a request', () => { + const toTest = password(undefined, { type: REQUEST(ACTION_TYPES.UPDATE_PASSWORD) }); + expect(toTest).toMatchObject({ + updateSuccess: false, + updateFailure: false, + loading: true, + }); + }); + it('should detect a success', () => { + const toTest = password(undefined, { type: SUCCESS(ACTION_TYPES.UPDATE_PASSWORD) }); + expect(toTest).toMatchObject({ + updateSuccess: true, + updateFailure: false, + loading: false, + }); + }); + it('should detect a failure', () => { + const toTest = password(undefined, { type: FAILURE(ACTION_TYPES.UPDATE_PASSWORD) }); + expect(toTest).toMatchObject({ + updateSuccess: false, + updateFailure: true, + loading: false, + }); + }); + + it('should reset the state', () => { + const initialState = { + loading: false, + errorMessage: null, + updateSuccess: false, + updateFailure: false, + }; + expect( + password( + { ...initialState, loading: true }, + { + type: ACTION_TYPES.RESET, + } + ) + ).toEqual({ + ...initialState, + }); + }); + }); + + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + beforeEach(() => { + const mockStore = configureStore([thunk, promiseMiddleware]); + store = mockStore({}); + axios.post = sinon.stub().returns(Promise.resolve(resolvedObject)); + }); + + it('dispatches UPDATE_PASSWORD_PENDING and UPDATE_PASSWORD_FULFILLED actions', async () => { + const meta = { + errorMessage: 'An error has occurred! The password could not be changed.', + successMessage: 'Password changed!', + }; + + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.UPDATE_PASSWORD), + meta, + }, + { + type: SUCCESS(ACTION_TYPES.UPDATE_PASSWORD), + payload: resolvedObject, + meta, + }, + ]; + await store.dispatch(savePassword('', '')).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + it('dispatches ACTION_TYPES.RESET actions', async () => { + const expectedActions = [ + { + type: ACTION_TYPES.RESET, + }, + ]; + await store.dispatch(reset()); + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/src/main/webapp/app/modules/account/password/password.reducer.ts b/src/main/webapp/app/modules/account/password/password.reducer.ts new file mode 100644 index 0000000..fb55526 --- /dev/null +++ b/src/main/webapp/app/modules/account/password/password.reducer.ts @@ -0,0 +1,66 @@ +import axios from 'axios'; + +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; + +export const ACTION_TYPES = { + UPDATE_PASSWORD: 'account/UPDATE_PASSWORD', + RESET: 'account/RESET', +}; + +const initialState = { + loading: false, + errorMessage: null, + updateSuccess: false, + updateFailure: false, +}; + +export type PasswordState = Readonly; + +// Reducer +export default (state: PasswordState = initialState, action): PasswordState => { + switch (action.type) { + case REQUEST(ACTION_TYPES.UPDATE_PASSWORD): + return { + ...initialState, + errorMessage: null, + updateSuccess: false, + loading: true, + }; + case FAILURE(ACTION_TYPES.UPDATE_PASSWORD): + return { + ...initialState, + loading: false, + updateSuccess: false, + updateFailure: true, + }; + case SUCCESS(ACTION_TYPES.UPDATE_PASSWORD): + return { + ...initialState, + loading: false, + updateSuccess: true, + updateFailure: false, + }; + case ACTION_TYPES.RESET: + return { + ...initialState, + }; + default: + return state; + } +}; + +// Actions +const apiUrl = 'api/account'; + +export const savePassword = (currentPassword, newPassword) => ({ + type: ACTION_TYPES.UPDATE_PASSWORD, + payload: axios.post(`${apiUrl}/change-password`, { currentPassword, newPassword }), + meta: { + successMessage: 'Password changed!', + errorMessage: 'An error has occurred! The password could not be changed.', + }, +}); + +export const reset = () => ({ + type: ACTION_TYPES.RESET, +}); diff --git a/src/main/webapp/app/modules/account/password/password.tsx b/src/main/webapp/app/modules/account/password/password.tsx new file mode 100644 index 0000000..0ecc5f1 --- /dev/null +++ b/src/main/webapp/app/modules/account/password/password.tsx @@ -0,0 +1,106 @@ +import React, { useState, useEffect } from 'react'; + +import { connect } from 'react-redux'; +import { AvForm, AvField } from 'availity-reactstrap-validation'; +import { Row, Col, Button } from 'reactstrap'; + +import { IRootState } from 'app/shared/reducers'; +import { getSession } from 'app/shared/reducers/authentication'; +import PasswordStrengthBar from 'app/shared/layout/password/password-strength-bar'; +import { savePassword, reset } from './password.reducer'; + +export interface IUserPasswordProps extends StateProps, DispatchProps {} + +export const PasswordPage = (props: IUserPasswordProps) => { + const [password, setPassword] = useState(''); + + useEffect(() => { + props.reset(); + props.getSession(); + return () => { + props.reset(); + }; + }, []); + + const handleValidSubmit = (event, values) => { + props.savePassword(values.currentPassword, values.newPassword); + }; + + const updatePassword = event => setPassword(event.target.value); + + return ( +
+ + +

Password for {props.account.login}

+ + + + + + + + +
+
+ ); +}; + +const mapStateToProps = ({ authentication }: IRootState) => ({ + account: authentication.account, + isAuthenticated: authentication.isAuthenticated, +}); + +const mapDispatchToProps = { getSession, savePassword, reset }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(PasswordPage); diff --git a/src/main/webapp/app/modules/account/register/register.reducer.spec.ts b/src/main/webapp/app/modules/account/register/register.reducer.spec.ts new file mode 100644 index 0000000..8a9be03 --- /dev/null +++ b/src/main/webapp/app/modules/account/register/register.reducer.spec.ts @@ -0,0 +1,103 @@ +import thunk from 'redux-thunk'; +import axios from 'axios'; +import sinon from 'sinon'; +import configureStore from 'redux-mock-store'; +import promiseMiddleware from 'redux-promise-middleware'; + +import { FAILURE, REQUEST, SUCCESS } from 'app/shared/reducers/action-type.util'; +import register, { ACTION_TYPES, handleRegister, reset } from './register.reducer'; + +describe('Creating account tests', () => { + const initialState = { + loading: false, + registrationSuccess: false, + registrationFailure: false, + errorMessage: null, + }; + + it('should return the initial state', () => { + expect(register(undefined, {})).toEqual({ + ...initialState, + }); + }); + + it('should detect a request', () => { + expect(register(undefined, { type: REQUEST(ACTION_TYPES.CREATE_ACCOUNT) })).toEqual({ + ...initialState, + loading: true, + }); + }); + + it('should handle RESET', () => { + expect( + register({ loading: true, registrationSuccess: true, registrationFailure: true, errorMessage: '' }, { type: ACTION_TYPES.RESET }) + ).toEqual({ + ...initialState, + }); + }); + + it('should handle CREATE_ACCOUNT success', () => { + expect( + register(undefined, { + type: SUCCESS(ACTION_TYPES.CREATE_ACCOUNT), + payload: 'fake payload', + }) + ).toEqual({ + ...initialState, + registrationSuccess: true, + }); + }); + + it('should handle CREATE_ACCOUNT failure', () => { + const payload = { response: { data: { errorKey: 'fake error' } } }; + expect( + register(undefined, { + type: FAILURE(ACTION_TYPES.CREATE_ACCOUNT), + payload, + }) + ).toEqual({ + ...initialState, + registrationFailure: true, + errorMessage: payload.response.data.errorKey, + }); + }); + + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + beforeEach(() => { + const mockStore = configureStore([thunk, promiseMiddleware]); + store = mockStore({}); + axios.post = sinon.stub().returns(Promise.resolve(resolvedObject)); + }); + + it('dispatches CREATE_ACCOUNT_PENDING and CREATE_ACCOUNT_FULFILLED actions', async () => { + const meta = { + successMessage: 'Registration saved! Please check your email for confirmation.', + }; + + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.CREATE_ACCOUNT), + meta, + }, + { + type: SUCCESS(ACTION_TYPES.CREATE_ACCOUNT), + payload: resolvedObject, + meta, + }, + ]; + await store.dispatch(handleRegister('', '', '')).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + it('dispatches ACTION_TYPES.RESET actions', async () => { + const expectedActions = [ + { + type: ACTION_TYPES.RESET, + }, + ]; + await store.dispatch(reset()); + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/src/main/webapp/app/modules/account/register/register.reducer.ts b/src/main/webapp/app/modules/account/register/register.reducer.ts new file mode 100644 index 0000000..4760507 --- /dev/null +++ b/src/main/webapp/app/modules/account/register/register.reducer.ts @@ -0,0 +1,58 @@ +import axios from 'axios'; + +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; + +export const ACTION_TYPES = { + CREATE_ACCOUNT: 'register/CREATE_ACCOUNT', + RESET: 'register/RESET', +}; + +const initialState = { + loading: false, + registrationSuccess: false, + registrationFailure: false, + errorMessage: null, +}; + +export type RegisterState = Readonly; + +// Reducer +export default (state: RegisterState = initialState, action): RegisterState => { + switch (action.type) { + case REQUEST(ACTION_TYPES.CREATE_ACCOUNT): + return { + ...state, + loading: true, + }; + case FAILURE(ACTION_TYPES.CREATE_ACCOUNT): + return { + ...initialState, + registrationFailure: true, + errorMessage: action.payload.response.data.errorKey, + }; + case SUCCESS(ACTION_TYPES.CREATE_ACCOUNT): + return { + ...initialState, + registrationSuccess: true, + }; + case ACTION_TYPES.RESET: + return { + ...initialState, + }; + default: + return state; + } +}; + +// Actions +export const handleRegister = (login, email, password, langKey = 'en') => ({ + type: ACTION_TYPES.CREATE_ACCOUNT, + payload: axios.post('api/register', { login, email, password, langKey }), + meta: { + successMessage: 'Registration saved! Please check your email for confirmation.', + }, +}); + +export const reset = () => ({ + type: ACTION_TYPES.RESET, +}); diff --git a/src/main/webapp/app/modules/account/register/register.tsx b/src/main/webapp/app/modules/account/register/register.tsx new file mode 100644 index 0000000..c46717a --- /dev/null +++ b/src/main/webapp/app/modules/account/register/register.tsx @@ -0,0 +1,119 @@ +import React, { useState, useEffect } from 'react'; + +import { connect } from 'react-redux'; +import { AvForm, AvField } from 'availity-reactstrap-validation'; +import { Row, Col, Alert, Button } from 'reactstrap'; + +import PasswordStrengthBar from 'app/shared/layout/password/password-strength-bar'; +import { IRootState } from 'app/shared/reducers'; +import { handleRegister, reset } from './register.reducer'; + +export type IRegisterProps = DispatchProps; + +export const RegisterPage = (props: IRegisterProps) => { + const [password, setPassword] = useState(''); + + useEffect( + () => () => { + props.reset(); + }, + [] + ); + + const handleValidSubmit = (event, values) => { + props.handleRegister(values.username, values.email, values.firstPassword); + event.preventDefault(); + }; + + const updatePassword = event => setPassword(event.target.value); + + return ( +
+ + +

+ Registration +

+ +
+ + + + + + + + + + +

 

+ + If you want to + sign in + + , you can try the default accounts: +
- Administrator (login="admin" and password="admin") +
- User (login="user" and password="user"). +
+
+ +
+
+ ); +}; + +const mapDispatchToProps = { handleRegister, reset }; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(null, mapDispatchToProps)(RegisterPage); diff --git a/src/main/webapp/app/modules/account/settings/settings.reducer.spec.ts b/src/main/webapp/app/modules/account/settings/settings.reducer.spec.ts new file mode 100644 index 0000000..37f4eb5 --- /dev/null +++ b/src/main/webapp/app/modules/account/settings/settings.reducer.spec.ts @@ -0,0 +1,116 @@ +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; +import configureStore from 'redux-mock-store'; +import promiseMiddleware from 'redux-promise-middleware'; +import thunk from 'redux-thunk'; +import axios from 'axios'; +import sinon from 'sinon'; + +import account, { ACTION_TYPES, saveAccountSettings, reset } from './settings.reducer'; +import { ACTION_TYPES as authActionTypes } from 'app/shared/reducers/authentication'; + +describe('Settings reducer tests', () => { + describe('Common tests', () => { + it('should return the initial state', () => { + const toTest = account(undefined, {}); + expect(toTest).toMatchObject({ + loading: false, + errorMessage: null, + updateSuccess: false, + updateFailure: false, + }); + }); + }); + + describe('Settings update', () => { + it('should detect a request', () => { + const toTest = account(undefined, { type: REQUEST(ACTION_TYPES.UPDATE_ACCOUNT) }); + expect(toTest).toMatchObject({ + updateSuccess: false, + updateFailure: false, + loading: true, + }); + }); + it('should detect a success', () => { + const toTest = account(undefined, { type: SUCCESS(ACTION_TYPES.UPDATE_ACCOUNT) }); + expect(toTest).toMatchObject({ + updateSuccess: true, + updateFailure: false, + loading: false, + }); + }); + it('should detect a failure', () => { + const toTest = account(undefined, { type: FAILURE(ACTION_TYPES.UPDATE_ACCOUNT) }); + expect(toTest).toMatchObject({ + updateSuccess: false, + updateFailure: true, + loading: false, + }); + }); + + it('should reset the state', () => { + const initialState = { + loading: false, + errorMessage: null, + updateSuccess: false, + updateFailure: false, + }; + expect( + account( + { ...initialState, loading: true }, + { + type: ACTION_TYPES.RESET, + } + ) + ).toEqual({ + ...initialState, + }); + }); + }); + + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + beforeEach(() => { + const mockStore = configureStore([thunk, promiseMiddleware]); + store = mockStore({}); + axios.get = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.post = sinon.stub().returns(Promise.resolve(resolvedObject)); + }); + + it('dispatches UPDATE_ACCOUNT_PENDING and UPDATE_ACCOUNT_FULFILLED actions', async () => { + const meta = { + successMessage: 'Settings saved!', + }; + + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.UPDATE_ACCOUNT), + meta, + }, + { + type: SUCCESS(ACTION_TYPES.UPDATE_ACCOUNT), + payload: resolvedObject, + meta, + }, + { + type: REQUEST(authActionTypes.GET_SESSION), + }, + { + type: SUCCESS(authActionTypes.GET_SESSION), + payload: resolvedObject, + }, + ]; + await store.dispatch(saveAccountSettings({})).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + it('dispatches ACTION_TYPES.RESET actions', async () => { + const expectedActions = [ + { + type: ACTION_TYPES.RESET, + }, + ]; + await store.dispatch(reset()); + expect(store.getActions()).toEqual(expectedActions); + }); + }); +}); diff --git a/src/main/webapp/app/modules/account/settings/settings.reducer.ts b/src/main/webapp/app/modules/account/settings/settings.reducer.ts new file mode 100644 index 0000000..0560269 --- /dev/null +++ b/src/main/webapp/app/modules/account/settings/settings.reducer.ts @@ -0,0 +1,70 @@ +import axios from 'axios'; + +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; +import { getSession } from 'app/shared/reducers/authentication'; + +export const ACTION_TYPES = { + UPDATE_ACCOUNT: 'account/UPDATE_ACCOUNT', + RESET: 'account/RESET', +}; + +const initialState = { + loading: false, + errorMessage: null, + updateSuccess: false, + updateFailure: false, +}; + +export type SettingsState = Readonly; + +// Reducer +export default (state: SettingsState = initialState, action): SettingsState => { + switch (action.type) { + case REQUEST(ACTION_TYPES.UPDATE_ACCOUNT): + return { + ...state, + errorMessage: null, + updateSuccess: false, + loading: true, + }; + case FAILURE(ACTION_TYPES.UPDATE_ACCOUNT): + return { + ...state, + loading: false, + updateSuccess: false, + updateFailure: true, + }; + case SUCCESS(ACTION_TYPES.UPDATE_ACCOUNT): + return { + ...state, + loading: false, + updateSuccess: true, + updateFailure: false, + }; + case ACTION_TYPES.RESET: + return { + ...initialState, + }; + default: + return state; + } +}; + +// Actions +const apiUrl = 'api/account'; + +export const saveAccountSettings: (account: any) => void = account => async dispatch => { + await dispatch({ + type: ACTION_TYPES.UPDATE_ACCOUNT, + payload: axios.post(apiUrl, account), + meta: { + successMessage: 'Settings saved!', + }, + }); + + await dispatch(getSession()); +}; + +export const reset = () => ({ + type: ACTION_TYPES.RESET, +}); diff --git a/src/main/webapp/app/modules/account/settings/settings.tsx b/src/main/webapp/app/modules/account/settings/settings.tsx new file mode 100644 index 0000000..9196c59 --- /dev/null +++ b/src/main/webapp/app/modules/account/settings/settings.tsx @@ -0,0 +1,101 @@ +import React, { useEffect } from 'react'; +import { Button, Col, Row } from 'reactstrap'; +import { connect } from 'react-redux'; + +import { AvForm, AvField } from 'availity-reactstrap-validation'; + +import { IRootState } from 'app/shared/reducers'; +import { getSession } from 'app/shared/reducers/authentication'; +import { saveAccountSettings, reset } from './settings.reducer'; + +export interface IUserSettingsProps extends StateProps, DispatchProps {} + +export const SettingsPage = (props: IUserSettingsProps) => { + useEffect(() => { + props.getSession(); + return () => { + props.reset(); + }; + }, []); + + const handleValidSubmit = (event, values) => { + const account = { + ...props.account, + ...values, + }; + + props.saveAccountSettings(account); + event.persist(); + }; + + return ( +
+ + +

User settings for {props.account.login}

+ + {/* First name */} + + {/* Last name */} + + {/* Email */} + + + + +
+
+ ); +}; + +const mapStateToProps = ({ authentication }: IRootState) => ({ + account: authentication.account, + isAuthenticated: authentication.isAuthenticated, +}); + +const mapDispatchToProps = { getSession, saveAccountSettings, reset }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(SettingsPage); diff --git a/src/main/webapp/app/modules/administration/administration.reducer.spec.ts b/src/main/webapp/app/modules/administration/administration.reducer.spec.ts new file mode 100644 index 0000000..76520dd --- /dev/null +++ b/src/main/webapp/app/modules/administration/administration.reducer.spec.ts @@ -0,0 +1,273 @@ +import configureStore from 'redux-mock-store'; +import promiseMiddleware from 'redux-promise-middleware'; +import axios from 'axios'; +import thunk from 'redux-thunk'; +import sinon from 'sinon'; + +import { REQUEST, FAILURE, SUCCESS } from 'app/shared/reducers/action-type.util'; +import administration, { + ACTION_TYPES, + systemHealth, + systemMetrics, + systemThreadDump, + getLoggers, + changeLogLevel, + getConfigurations, + getEnv, +} from './administration.reducer'; + +describe('Administration reducer tests', () => { + const username = process.env.E2E_USERNAME ?? 'admin'; + + function isEmpty(element): boolean { + if (element instanceof Array) { + return element.length === 0; + } else { + return Object.keys(element).length === 0; + } + } + + function testInitialState(state) { + expect(state).toMatchObject({ + loading: false, + errorMessage: null, + totalItems: 0, + }); + expect(isEmpty(state.logs.loggers)); + expect(isEmpty(state.threadDump)); + } + + function testMultipleTypes(types, payload, testFunction) { + types.forEach(e => { + testFunction(administration(undefined, { type: e, payload })); + }); + } + + describe('Common', () => { + it('should return the initial state', () => { + testInitialState(administration(undefined, {})); + }); + }); + + describe('Requests', () => { + it('should set state to loading', () => { + testMultipleTypes( + [ + REQUEST(ACTION_TYPES.FETCH_LOGS), + REQUEST(ACTION_TYPES.FETCH_HEALTH), + REQUEST(ACTION_TYPES.FETCH_METRICS), + REQUEST(ACTION_TYPES.FETCH_THREAD_DUMP), + REQUEST(ACTION_TYPES.FETCH_CONFIGURATIONS), + REQUEST(ACTION_TYPES.FETCH_ENV), + ], + {}, + state => { + expect(state).toMatchObject({ + errorMessage: null, + loading: true, + }); + } + ); + }); + }); + + describe('Failures', () => { + it('should set state to failed and put an error message in errorMessage', () => { + testMultipleTypes( + [ + FAILURE(ACTION_TYPES.FETCH_LOGS), + FAILURE(ACTION_TYPES.FETCH_HEALTH), + FAILURE(ACTION_TYPES.FETCH_METRICS), + FAILURE(ACTION_TYPES.FETCH_THREAD_DUMP), + FAILURE(ACTION_TYPES.FETCH_CONFIGURATIONS), + FAILURE(ACTION_TYPES.FETCH_ENV), + ], + 'something happened', + state => { + expect(state).toMatchObject({ + loading: false, + errorMessage: 'something happened', + }); + } + ); + }); + }); + + describe('Success', () => { + it('should update state according to a successful fetch logs request', () => { + const payload = { + data: { + loggers: { + main: { + effectiveLevel: 'WARN', + }, + }, + }, + }; + const toTest = administration(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_LOGS), payload }); + + expect(toTest).toMatchObject({ + loading: false, + logs: payload.data, + }); + }); + + it('should update state according to a successful fetch health request', () => { + const payload = { data: { status: 'UP' } }; + const toTest = administration(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_HEALTH), payload }); + + expect(toTest).toMatchObject({ + loading: false, + health: payload.data, + }); + }); + + it('should update state according to a successful fetch metrics request', () => { + const payload = { data: { version: '3.1.3', gauges: {} } }; + const toTest = administration(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_METRICS), payload }); + + expect(toTest).toMatchObject({ + loading: false, + metrics: payload.data, + }); + }); + + it('should update state according to a successful fetch thread dump request', () => { + const payload = { data: [{ threadName: 'hz.gateway.cached.thread-6', threadId: 9266 }] }; + const toTest = administration(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_THREAD_DUMP), payload }); + + expect(toTest).toMatchObject({ + loading: false, + threadDump: payload.data, + }); + }); + + it('should update state according to a successful fetch configurations request', () => { + const payload = { data: { contexts: { jhipster: { beans: {} } } } }; + const toTest = administration(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_CONFIGURATIONS), payload }); + + expect(toTest).toMatchObject({ + loading: false, + configuration: { + configProps: payload.data, + env: {}, + }, + }); + }); + + it('should update state according to a successful fetch env request', () => { + const payload = { data: { activeProfiles: ['api-docs', 'dev'] } }; + const toTest = administration(undefined, { type: SUCCESS(ACTION_TYPES.FETCH_ENV), payload }); + + expect(toTest).toMatchObject({ + loading: false, + configuration: { + configProps: {}, + env: payload.data, + }, + }); + }); + }); + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + beforeEach(() => { + const mockStore = configureStore([thunk, promiseMiddleware]); + store = mockStore({}); + axios.get = sinon.stub().returns(Promise.resolve(resolvedObject)); + axios.post = sinon.stub().returns(Promise.resolve(resolvedObject)); + }); + it('dispatches FETCH_HEALTH_PENDING and FETCH_HEALTH_FULFILLED actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_HEALTH), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_HEALTH), + payload: resolvedObject, + }, + ]; + await store.dispatch(systemHealth()).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + it('dispatches FETCH_METRICS_PENDING and FETCH_METRICS_FULFILLED actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_METRICS), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_METRICS), + payload: resolvedObject, + }, + ]; + await store.dispatch(systemMetrics()).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + it('dispatches FETCH_THREAD_DUMP_PENDING and FETCH_THREAD_DUMP_FULFILLED actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_THREAD_DUMP), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_THREAD_DUMP), + payload: resolvedObject, + }, + ]; + await store.dispatch(systemThreadDump()).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + it('dispatches FETCH_LOGS_PENDING and FETCH_LOGS_FULFILLED actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_LOGS), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_LOGS), + payload: resolvedObject, + }, + ]; + await store.dispatch(getLoggers()).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + it('dispatches FETCH_LOGS_CHANGE_LEVEL_PENDING and FETCH_LOGS_CHANGE_LEVEL_FULFILLED actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_LOGS_CHANGE_LEVEL), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_LOGS_CHANGE_LEVEL), + payload: resolvedObject, + }, + { + type: REQUEST(ACTION_TYPES.FETCH_LOGS), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_LOGS), + payload: resolvedObject, + }, + ]; + await store.dispatch(changeLogLevel('ROOT', 'DEBUG')).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + it('dispatches FETCH_CONFIGURATIONS_PENDING and FETCH_CONFIGURATIONS_FULFILLED actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_CONFIGURATIONS), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_CONFIGURATIONS), + payload: resolvedObject, + }, + ]; + await store.dispatch(getConfigurations()).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + it('dispatches FETCH_ENV_PENDING and FETCH_ENV_FULFILLED actions', async () => { + const expectedActions = [ + { + type: REQUEST(ACTION_TYPES.FETCH_ENV), + }, + { + type: SUCCESS(ACTION_TYPES.FETCH_ENV), + payload: resolvedObject, + }, + ]; + await store.dispatch(getEnv()).then(() => expect(store.getActions()).toEqual(expectedActions)); + }); + }); +}); diff --git a/src/main/webapp/app/modules/administration/administration.reducer.ts b/src/main/webapp/app/modules/administration/administration.reducer.ts new file mode 100644 index 0000000..5dfa8ed --- /dev/null +++ b/src/main/webapp/app/modules/administration/administration.reducer.ts @@ -0,0 +1,149 @@ +import axios from 'axios'; + +import { REQUEST, SUCCESS, FAILURE } from 'app/shared/reducers/action-type.util'; + +export const ACTION_TYPES = { + FETCH_LOGS: 'administration/FETCH_LOGS', + FETCH_LOGS_CHANGE_LEVEL: 'administration/FETCH_LOGS_CHANGE_LEVEL', + FETCH_HEALTH: 'administration/FETCH_HEALTH', + FETCH_METRICS: 'administration/FETCH_METRICS', + FETCH_THREAD_DUMP: 'administration/FETCH_THREAD_DUMP', + FETCH_CONFIGURATIONS: 'administration/FETCH_CONFIGURATIONS', + FETCH_ENV: 'administration/FETCH_ENV', +}; + +const initialState = { + loading: false, + errorMessage: null, + logs: { + loggers: [] as any[], + }, + health: {} as any, + metrics: {} as any, + threadDump: [], + configuration: { + configProps: {} as any, + env: {} as any, + }, + totalItems: 0, +}; + +export type AdministrationState = Readonly; + +// Reducer + +export default (state: AdministrationState = initialState, action): AdministrationState => { + switch (action.type) { + case REQUEST(ACTION_TYPES.FETCH_METRICS): + case REQUEST(ACTION_TYPES.FETCH_THREAD_DUMP): + case REQUEST(ACTION_TYPES.FETCH_LOGS): + case REQUEST(ACTION_TYPES.FETCH_CONFIGURATIONS): + case REQUEST(ACTION_TYPES.FETCH_ENV): + case REQUEST(ACTION_TYPES.FETCH_HEALTH): + return { + ...state, + errorMessage: null, + loading: true, + }; + case FAILURE(ACTION_TYPES.FETCH_METRICS): + case FAILURE(ACTION_TYPES.FETCH_THREAD_DUMP): + case FAILURE(ACTION_TYPES.FETCH_LOGS): + case FAILURE(ACTION_TYPES.FETCH_CONFIGURATIONS): + case FAILURE(ACTION_TYPES.FETCH_ENV): + case FAILURE(ACTION_TYPES.FETCH_HEALTH): + return { + ...state, + loading: false, + errorMessage: action.payload, + }; + case SUCCESS(ACTION_TYPES.FETCH_METRICS): + return { + ...state, + loading: false, + metrics: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.FETCH_THREAD_DUMP): + return { + ...state, + loading: false, + threadDump: action.payload.data, + }; + case SUCCESS(ACTION_TYPES.FETCH_LOGS): + return { + ...state, + loading: false, + logs: { + loggers: action.payload.data.loggers, + }, + }; + case SUCCESS(ACTION_TYPES.FETCH_CONFIGURATIONS): + return { + ...state, + loading: false, + configuration: { + ...state.configuration, + configProps: action.payload.data, + }, + }; + case SUCCESS(ACTION_TYPES.FETCH_ENV): + return { + ...state, + loading: false, + configuration: { + ...state.configuration, + env: action.payload.data, + }, + }; + case SUCCESS(ACTION_TYPES.FETCH_HEALTH): + return { + ...state, + loading: false, + health: action.payload.data, + }; + default: + return state; + } +}; + +// Actions + +export const systemHealth = () => ({ + type: ACTION_TYPES.FETCH_HEALTH, + payload: axios.get('management/health'), +}); + +export const systemMetrics = () => ({ + type: ACTION_TYPES.FETCH_METRICS, + payload: axios.get('management/jhimetrics'), +}); + +export const systemThreadDump = () => ({ + type: ACTION_TYPES.FETCH_THREAD_DUMP, + payload: axios.get('management/threaddump'), +}); + +export const getLoggers = () => ({ + type: ACTION_TYPES.FETCH_LOGS, + payload: axios.get('management/loggers'), +}); + +export const changeLogLevel: (name, configuredLevel) => void = (name, configuredLevel) => { + const body = { configuredLevel }; + return async dispatch => { + await dispatch({ + type: ACTION_TYPES.FETCH_LOGS_CHANGE_LEVEL, + payload: axios.post('management/loggers/' + name, body), + }); + dispatch(getLoggers()); + }; +}; + +export const getConfigurations = () => ({ + type: ACTION_TYPES.FETCH_CONFIGURATIONS, + payload: axios.get('management/configprops'), +}); + +export const getEnv = () => ({ + type: ACTION_TYPES.FETCH_ENV, + payload: axios.get('management/env'), +}); diff --git a/src/main/webapp/app/modules/administration/configuration/configuration.tsx b/src/main/webapp/app/modules/administration/configuration/configuration.tsx new file mode 100644 index 0000000..c988f05 --- /dev/null +++ b/src/main/webapp/app/modules/administration/configuration/configuration.tsx @@ -0,0 +1,120 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { Table, Input, Row, Col, Badge } from 'reactstrap'; + +import { getConfigurations, getEnv } from '../administration.reducer'; +import { IRootState } from 'app/shared/reducers'; + +export interface IConfigurationPageProps extends StateProps, DispatchProps {} + +export const ConfigurationPage = (props: IConfigurationPageProps) => { + const [filter, setFilter] = useState(''); + const [reversePrefix, setReversePrefix] = useState(false); + const [reverseProperties, setReverseProperties] = useState(false); + + useEffect(() => { + props.getConfigurations(); + props.getEnv(); + }, []); + + const changeFilter = evt => setFilter(evt.target.value); + + const envFilterFn = configProp => configProp.toUpperCase().includes(filter.toUpperCase()); + + const propsFilterFn = configProp => configProp.prefix.toUpperCase().includes(filter.toUpperCase()); + + const changeReversePrefix = () => setReversePrefix(!reversePrefix); + + const changeReverseProperties = () => setReverseProperties(!reverseProperties); + + const getContextList = contexts => + Object.values(contexts) + .map((v: any) => v.beans) + .reduce((acc, e) => ({ ...acc, ...e })); + + const { configuration } = props; + + const configProps = configuration && configuration.configProps ? configuration.configProps : {}; + + const env = configuration && configuration.env ? configuration.env : {}; + + return ( +
+

+ Configuration +

+ Filter + + + + + + + + + + {configProps.contexts + ? Object.values(getContextList(configProps.contexts)) + .filter(propsFilterFn) + .map((property: any, propIndex) => ( + + + {propKey} + + {JSON.stringify(property['properties'][propKey])} + + + ))} + + + )) + : null} + +
PrefixProperties
{property['prefix']} + {Object.keys(property['properties']).map((propKey, index) => ( + +
+ {env.propertySources + ? env.propertySources.map((envKey, envIndex) => ( +
+

+ {envKey.name} +

+ + + + + + + + + {Object.keys(envKey.properties) + .filter(envFilterFn) + .map((propKey, propIndex) => ( + + + + + ))} + +
PropertyValue
{propKey} + {envKey.properties[propKey].value} +
+
+ )) + : null} +
+ ); +}; + +const mapStateToProps = ({ administration }: IRootState) => ({ + configuration: administration.configuration, + isFetching: administration.loading, +}); + +const mapDispatchToProps = { getConfigurations, getEnv }; + +type StateProps = ReturnType; +type DispatchProps = typeof mapDispatchToProps; + +export default connect(mapStateToProps, mapDispatchToProps)(ConfigurationPage); diff --git a/src/main/webapp/app/modules/administration/docs/docs.scss b/src/main/webapp/app/modules/administration/docs/docs.scss new file mode 100644 index 0000000..b5edede --- /dev/null +++ b/src/main/webapp/app/modules/administration/docs/docs.scss @@ -0,0 +1,3 @@ +iframe { + background: white; +} diff --git a/src/main/webapp/app/modules/administration/docs/docs.tsx b/src/main/webapp/app/modules/administration/docs/docs.tsx new file mode 100644 index 0000000..37c5e1b --- /dev/null +++ b/src/main/webapp/app/modules/administration/docs/docs.tsx @@ -0,0 +1,19 @@ +import './docs.scss'; + +import React from 'react'; + +const DocsPage = () => ( +
+