From 12e0fdda2d0f2086775f56547bc432f39c71c9b0 Mon Sep 17 00:00:00 2001 From: JHipster Bot Date: Wed, 10 Apr 2024 14:19:18 +0000 Subject: [PATCH] Initial application generation by JHipster --- .devcontainer/Dockerfile | 25 + .devcontainer/devcontainer.json | 52 + .editorconfig | 23 + .eslintignore | 10 + .eslintrc.json | 79 + .gitattributes | 150 + .gitignore | 153 + .husky/pre-commit | 5 + .lintstagedrc.cjs | 3 + .mvn/jvm.config | 1 + .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .mvn/wrapper/maven-wrapper.properties | 18 + .prettierignore | 11 + .prettierrc | 22 + .yo-rc.json | 40 + README.md | 237 ++ checkstyle.xml | 20 + jest.conf.js | 67 + mvnw | 308 ++ mvnw.cmd | 205 + npmw | 42 + npmw.cmd | 31 + package.json | 168 + pom.xml | 1194 ++++++ postcss.config.js | 3 + sonar-project.properties | 43 + 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 | 51 + src/main/docker/jib/entrypoint.sh | 39 + src/main/docker/monitoring.yml | 31 + src/main/docker/postgresql.yml | 20 + src/main/docker/prometheus/prometheus.yml | 31 + src/main/docker/services.yml | 7 + src/main/docker/sonar.yml | 15 + .../familytree/ApplicationWebXml.java | 19 + .../familytree/GeneratedByJHipster.java | 13 + .../familytree/NayaksFamilyApp.java | 108 + .../familytree/aop/logging/LoggingAspect.java | 113 + .../familytree/aop/logging/package-info.java | 4 + .../config/ApplicationProperties.java | 37 + .../familytree/config/AsyncConfiguration.java | 48 + .../familytree/config/CRLFLogConverter.java | 69 + .../familytree/config/CacheConfiguration.java | 80 + .../familytree/config/Constants.java | 15 + .../config/DatabaseConfiguration.java | 57 + .../config/DateTimeFormatConfiguration.java | 20 + .../config/JacksonConfiguration.java | 34 + .../config/LiquibaseConfiguration.java | 81 + .../config/LocaleConfiguration.java | 24 + .../config/LoggingAspectConfiguration.java | 17 + .../config/LoggingConfiguration.java | 47 + .../config/SecurityConfiguration.java | 111 + .../config/SecurityJwtConfiguration.java | 76 + .../StaticResourcesWebConfiguration.java | 47 + .../familytree/config/WebConfigurer.java | 110 + .../familytree/config/package-info.java | 4 + .../domain/AbstractAuditingEntity.java | 75 + .../familytree/domain/Authority.java | 95 + .../nayakfamily/familytree/domain/User.java | 232 + .../familytree/domain/package-info.java | 4 + .../management/SecurityMetersService.java | 50 + .../familytree/management/package-info.java | 4 + .../nayakfamily/familytree/package-info.java | 4 + .../repository/AuthorityRepository.java | 12 + .../familytree/repository/UserRepository.java | 36 + .../familytree/repository/package-info.java | 4 + .../security/AuthoritiesConstants.java | 15 + .../security/DomainUserDetailsService.java | 62 + .../familytree/security/SecurityUtils.java | 106 + .../security/SpringSecurityAuditorAware.java | 18 + .../security/UserNotActivatedException.java | 19 + .../familytree/security/package-info.java | 4 + .../service/EmailAlreadyUsedException.java | 10 + .../service/InvalidPasswordException.java | 10 + .../familytree/service/MailService.java | 120 + .../familytree/service/UserService.java | 324 ++ .../service/UsernameAlreadyUsedException.java | 10 + .../familytree/service/dto/AdminUserDTO.java | 196 + .../service/dto/PasswordChangeDTO.java | 39 + .../familytree/service/dto/UserDTO.java | 51 + .../familytree/service/dto/package-info.java | 4 + .../familytree/service/mapper/UserMapper.java | 146 + .../service/mapper/package-info.java | 4 + .../familytree/service/package-info.java | 4 + .../familytree/web/filter/SpaWebFilter.java | 34 + .../familytree/web/filter/package-info.java | 4 + .../familytree/web/rest/AccountResource.java | 180 + .../web/rest/AuthenticateController.java | 124 + .../web/rest/AuthorityResource.java | 101 + .../web/rest/PublicUserResource.java | 56 + .../familytree/web/rest/UserResource.java | 213 + .../rest/errors/BadRequestAlertException.java | 49 + .../errors/EmailAlreadyUsedException.java | 11 + .../web/rest/errors/ErrorConstants.java | 17 + .../web/rest/errors/ExceptionTranslator.java | 264 ++ .../web/rest/errors/FieldErrorVM.java | 32 + .../rest/errors/InvalidPasswordException.java | 23 + .../errors/LoginAlreadyUsedException.java | 11 + .../web/rest/errors/package-info.java | 4 + .../familytree/web/rest/package-info.java | 4 + .../web/rest/vm/KeyAndPasswordVM.java | 27 + .../familytree/web/rest/vm/LoginVM.java | 53 + .../familytree/web/rest/vm/ManagedUserVM.java | 35 + .../familytree/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 | 109 + .../resources/config/application-prod.yml | 124 + src/main/resources/config/application-tls.yml | 19 + src/main/resources/config/application.yml | 213 + .../00000000000000_initial_schema.xml | 121 + .../config/liquibase/data/authority.csv | 3 + .../resources/config/liquibase/data/user.csv | 3 + .../config/liquibase/data/user_authority.csv | 4 + .../resources/config/liquibase/master.xml | 23 + src/main/resources/config/tls/keystore.p12 | Bin 0 -> 2768 bytes src/main/resources/i18n/messages.properties | 21 + src/main/resources/logback-spring.xml | 78 + src/main/resources/templates/error.html | 94 + .../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 | 315 ++ src/main/webapp/app/app.tsx | 63 + .../app/config/axios-interceptor.spec.ts | 32 + .../webapp/app/config/axios-interceptor.ts | 28 + src/main/webapp/app/config/constants.ts | 15 + src/main/webapp/app/config/dayjs.ts | 11 + .../webapp/app/config/error-middleware.ts | 29 + src/main/webapp/app/config/icon-loader.ts | 75 + .../webapp/app/config/logger-middleware.ts | 16 + .../config/notification-middleware.spec.ts | 241 ++ .../app/config/notification-middleware.ts | 102 + src/main/webapp/app/config/store.ts | 30 + src/main/webapp/app/entities/menu.tsx | 14 + src/main/webapp/app/entities/reducers.ts | 7 + src/main/webapp/app/entities/routes.tsx | 17 + src/main/webapp/app/index.tsx | 34 + .../account/activate/activate.reducer.spec.ts | 82 + .../account/activate/activate.reducer.ts | 42 + .../app/modules/account/activate/activate.tsx | 53 + src/main/webapp/app/modules/account/index.tsx | 18 + .../finish/password-reset-finish.tsx | 87 + .../init/password-reset-init.tsx | 63 + .../password-reset/password-reset.reducer.ts | 67 + .../account/password/password.reducer.spec.ts | 88 + .../account/password/password.reducer.ts | 64 + .../app/modules/account/password/password.tsx | 97 + .../account/register/register.reducer.spec.ts | 93 + .../account/register/register.reducer.ts | 53 + .../app/modules/account/register/register.tsx | 125 + .../account/settings/settings.reducer.spec.ts | 90 + .../account/settings/settings.reducer.ts | 63 + .../app/modules/account/settings/settings.tsx | 92 + .../administration.reducer.spec.ts | 232 + .../administration/administration.reducer.ts | 125 + .../configuration/configuration.tsx | 108 + .../app/modules/administration/docs/docs.scss | 3 + .../app/modules/administration/docs/docs.tsx | 19 + .../administration/health/health-modal.tsx | 46 + .../modules/administration/health/health.tsx | 89 + .../app/modules/administration/index.tsx | 25 + .../app/modules/administration/logs/logs.tsx | 105 + .../administration/metrics/metrics.tsx | 114 + .../administration/user-management/index.tsx | 22 + .../user-management-delete-dialog.tsx | 50 + .../user-management-detail.tsx | 74 + .../user-management-update.tsx | 156 + .../user-management.reducer.spec.ts | 286 ++ .../user-management.reducer.ts | 136 + .../user-management/user-management.tsx | 214 + src/main/webapp/app/modules/home/home.scss | 10 + src/main/webapp/app/modules/home/home.tsx | 88 + .../webapp/app/modules/login/login-modal.tsx | 97 + src/main/webapp/app/modules/login/login.tsx | 35 + src/main/webapp/app/modules/login/logout.tsx | 24 + src/main/webapp/app/routes.tsx | 74 + src/main/webapp/app/setup-tests.ts | 3 + src/main/webapp/app/shared/DurationFormat.tsx | 28 + .../app/shared/auth/private-route.spec.tsx | 123 + .../webapp/app/shared/auth/private-route.tsx | 67 + .../error/error-boundary-routes.spec.tsx | 55 + .../shared/error/error-boundary-routes.tsx | 21 + .../app/shared/error/error-boundary.spec.tsx | 30 + .../app/shared/error/error-boundary.tsx | 43 + .../app/shared/error/page-not-found.tsx | 13 + .../app/shared/layout/footer/footer.scss | 3 + .../app/shared/layout/footer/footer.tsx | 17 + .../layout/header/header-components.tsx | 28 + .../app/shared/layout/header/header.scss | 123 + .../app/shared/layout/header/header.spec.tsx | 109 + .../app/shared/layout/header/header.tsx | 55 + .../app/shared/layout/menus/account.spec.tsx | 52 + .../app/shared/layout/menus/account.tsx | 38 + .../webapp/app/shared/layout/menus/admin.tsx | 49 + .../app/shared/layout/menus/entities.tsx | 10 + .../webapp/app/shared/layout/menus/index.ts | 3 + .../shared/layout/menus/menu-components.tsx | 16 + .../app/shared/layout/menus/menu-item.tsx | 25 + .../password/password-strength-bar.scss | 23 + .../layout/password/password-strength-bar.tsx | 73 + .../webapp/app/shared/model/user.model.ts | 31 + .../reducers/application-profile.spec.ts | 72 + .../shared/reducers/application-profile.ts | 33 + .../shared/reducers/authentication.spec.ts | 228 + .../app/shared/reducers/authentication.ts | 156 + src/main/webapp/app/shared/reducers/index.ts | 31 + .../app/shared/reducers/reducer.utils.ts | 127 + 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 | 46 + .../app/shared/util/pagination.constants.ts | 4 + src/main/webapp/app/typings.d.ts | 8 + 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 | 117 + src/main/webapp/manifest.webapp | 31 + src/main/webapp/robots.txt | 10 + .../swagger-ui/dist/images/throbber.gif | Bin 0 -> 9257 bytes src/main/webapp/swagger-ui/index.html | 114 + .../familytree/IntegrationTest.java | 20 + .../familytree/TechnicalStructureTest.java | 38 + .../config/AsyncSyncConfiguration.java | 15 + .../config/CRLFLogConverterTest.java | 133 + .../familytree/config/EmbeddedSQL.java | 11 + .../config/PostgreSqlTestContainer.java | 41 + .../config/SpringBootTestClassOrderer.java | 22 + .../familytree/config/SqlTestContainer.java | 9 + ...tainersSpringContextCustomizerFactory.java | 67 + .../StaticResourcesWebConfigurerTest.java | 76 + .../familytree/config/WebConfigurerTest.java | 132 + .../config/WebConfigurerTestController.java | 14 + .../config/timezone/HibernateTimeZoneIT.java | 172 + .../familytree/domain/AssertUtils.java | 15 + .../familytree/domain/AuthorityAsserts.java | 56 + .../familytree/domain/AuthorityTest.java | 34 + .../domain/AuthorityTestSamples.java | 18 + .../SecurityMetersServiceTests.java | 70 + .../repository/timezone/DateTimeWrapper.java | 133 + .../timezone/DateTimeWrapperRepository.java | 10 + .../security/DomainUserDetailsServiceIT.java | 114 + .../security/SecurityUtilsUnitTest.java | 101 + .../jwt/AuthenticationIntegrationTest.java | 33 + .../jwt/JwtAuthenticationTestUtils.java | 105 + .../security/jwt/TokenAuthenticationIT.java | 53 + .../TokenAuthenticationSecurityMetersIT.java | 88 + .../familytree/service/MailServiceIT.java | 236 + .../familytree/service/UserServiceIT.java | 181 + .../service/mapper/UserMapperTest.java | 132 + .../familytree/web/filter/SpaWebFilterIT.java | 88 + .../web/rest/AccountResourceIT.java | 730 ++++ .../web/rest/AuthenticateControllerIT.java | 102 + .../web/rest/AuthorityResourceIT.java | 195 + .../web/rest/PublicUserResourceIT.java | 87 + .../familytree/web/rest/TestUtil.java | 202 + .../familytree/web/rest/UserResourceIT.java | 541 +++ .../web/rest/WithUnauthenticatedMockUser.java | 23 + .../rest/errors/ExceptionTranslatorIT.java | 117 + .../ExceptionTranslatorTestController.java | 66 + src/test/resources/META-INF/spring.factories | 2 + .../resources/config/application-testdev.yml | 36 + .../resources/config/application-testprod.yml | 35 + src/test/resources/config/application.yml | 90 + .../resources/i18n/messages_en.properties | 1 + src/test/resources/junit-platform.properties | 4 + src/test/resources/logback.xml | 48 + .../templates/mail/activationEmail.html | 19 + .../templates/mail/creationEmail.html | 19 + .../templates/mail/passwordResetEmail.html | 21 + .../resources/templates/mail/testEmail.html | 1 + tsconfig.json | 27 + tsconfig.test.json | 11 + webpack/environment.js | 9 + webpack/logo-jhipster.png | Bin 0 -> 3326 bytes webpack/utils.js | 39 + webpack/webpack.common.js | 133 + webpack/webpack.dev.js | 105 + webpack/webpack.prod.js | 112 + 305 files changed, 23564 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100755 .husky/pre-commit create mode 100644 .lintstagedrc.cjs create mode 100644 .mvn/jvm.config create mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .yo-rc.json create mode 100644 README.md create mode 100644 checkstyle.xml create mode 100644 jest.conf.js create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100755 npmw create mode 100644 npmw.cmd create mode 100644 package.json create mode 100644 pom.xml create mode 100644 postcss.config.js 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/postgresql.yml create mode 100644 src/main/docker/prometheus/prometheus.yml create mode 100644 src/main/docker/services.yml create mode 100644 src/main/docker/sonar.yml create mode 100644 src/main/java/com/nayakfamily/familytree/ApplicationWebXml.java create mode 100644 src/main/java/com/nayakfamily/familytree/GeneratedByJHipster.java create mode 100644 src/main/java/com/nayakfamily/familytree/NayaksFamilyApp.java create mode 100644 src/main/java/com/nayakfamily/familytree/aop/logging/LoggingAspect.java create mode 100644 src/main/java/com/nayakfamily/familytree/aop/logging/package-info.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/ApplicationProperties.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/AsyncConfiguration.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/CRLFLogConverter.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/CacheConfiguration.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/Constants.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/DatabaseConfiguration.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/DateTimeFormatConfiguration.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/JacksonConfiguration.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/LiquibaseConfiguration.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/LocaleConfiguration.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/LoggingAspectConfiguration.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/LoggingConfiguration.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/SecurityConfiguration.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/SecurityJwtConfiguration.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/StaticResourcesWebConfiguration.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/WebConfigurer.java create mode 100644 src/main/java/com/nayakfamily/familytree/config/package-info.java create mode 100644 src/main/java/com/nayakfamily/familytree/domain/AbstractAuditingEntity.java create mode 100644 src/main/java/com/nayakfamily/familytree/domain/Authority.java create mode 100644 src/main/java/com/nayakfamily/familytree/domain/User.java create mode 100644 src/main/java/com/nayakfamily/familytree/domain/package-info.java create mode 100644 src/main/java/com/nayakfamily/familytree/management/SecurityMetersService.java create mode 100644 src/main/java/com/nayakfamily/familytree/management/package-info.java create mode 100644 src/main/java/com/nayakfamily/familytree/package-info.java create mode 100644 src/main/java/com/nayakfamily/familytree/repository/AuthorityRepository.java create mode 100644 src/main/java/com/nayakfamily/familytree/repository/UserRepository.java create mode 100644 src/main/java/com/nayakfamily/familytree/repository/package-info.java create mode 100644 src/main/java/com/nayakfamily/familytree/security/AuthoritiesConstants.java create mode 100644 src/main/java/com/nayakfamily/familytree/security/DomainUserDetailsService.java create mode 100644 src/main/java/com/nayakfamily/familytree/security/SecurityUtils.java create mode 100644 src/main/java/com/nayakfamily/familytree/security/SpringSecurityAuditorAware.java create mode 100644 src/main/java/com/nayakfamily/familytree/security/UserNotActivatedException.java create mode 100644 src/main/java/com/nayakfamily/familytree/security/package-info.java create mode 100644 src/main/java/com/nayakfamily/familytree/service/EmailAlreadyUsedException.java create mode 100644 src/main/java/com/nayakfamily/familytree/service/InvalidPasswordException.java create mode 100644 src/main/java/com/nayakfamily/familytree/service/MailService.java create mode 100644 src/main/java/com/nayakfamily/familytree/service/UserService.java create mode 100644 src/main/java/com/nayakfamily/familytree/service/UsernameAlreadyUsedException.java create mode 100644 src/main/java/com/nayakfamily/familytree/service/dto/AdminUserDTO.java create mode 100644 src/main/java/com/nayakfamily/familytree/service/dto/PasswordChangeDTO.java create mode 100644 src/main/java/com/nayakfamily/familytree/service/dto/UserDTO.java create mode 100644 src/main/java/com/nayakfamily/familytree/service/dto/package-info.java create mode 100644 src/main/java/com/nayakfamily/familytree/service/mapper/UserMapper.java create mode 100644 src/main/java/com/nayakfamily/familytree/service/mapper/package-info.java create mode 100644 src/main/java/com/nayakfamily/familytree/service/package-info.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/filter/SpaWebFilter.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/filter/package-info.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/AccountResource.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/AuthenticateController.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/AuthorityResource.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/PublicUserResource.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/UserResource.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/errors/BadRequestAlertException.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/errors/EmailAlreadyUsedException.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/errors/ErrorConstants.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/errors/ExceptionTranslator.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/errors/FieldErrorVM.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/errors/InvalidPasswordException.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/errors/LoginAlreadyUsedException.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/errors/package-info.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/package-info.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/vm/KeyAndPasswordVM.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/vm/LoginVM.java create mode 100644 src/main/java/com/nayakfamily/familytree/web/rest/vm/ManagedUserVM.java create mode 100644 src/main/java/com/nayakfamily/familytree/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/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/master.xml create mode 100644 src/main/resources/config/tls/keystore.p12 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/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/menu.tsx create mode 100644 src/main/webapp/app/entities/reducers.ts create mode 100644 src/main/webapp/app/entities/routes.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-routes.spec.tsx create mode 100644 src/main/webapp/app/shared/error/error-boundary-routes.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/user.model.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/reducers/reducer.utils.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/nayakfamily/familytree/IntegrationTest.java create mode 100644 src/test/java/com/nayakfamily/familytree/TechnicalStructureTest.java create mode 100644 src/test/java/com/nayakfamily/familytree/config/AsyncSyncConfiguration.java create mode 100644 src/test/java/com/nayakfamily/familytree/config/CRLFLogConverterTest.java create mode 100644 src/test/java/com/nayakfamily/familytree/config/EmbeddedSQL.java create mode 100644 src/test/java/com/nayakfamily/familytree/config/PostgreSqlTestContainer.java create mode 100644 src/test/java/com/nayakfamily/familytree/config/SpringBootTestClassOrderer.java create mode 100644 src/test/java/com/nayakfamily/familytree/config/SqlTestContainer.java create mode 100644 src/test/java/com/nayakfamily/familytree/config/SqlTestContainersSpringContextCustomizerFactory.java create mode 100644 src/test/java/com/nayakfamily/familytree/config/StaticResourcesWebConfigurerTest.java create mode 100644 src/test/java/com/nayakfamily/familytree/config/WebConfigurerTest.java create mode 100644 src/test/java/com/nayakfamily/familytree/config/WebConfigurerTestController.java create mode 100644 src/test/java/com/nayakfamily/familytree/config/timezone/HibernateTimeZoneIT.java create mode 100644 src/test/java/com/nayakfamily/familytree/domain/AssertUtils.java create mode 100644 src/test/java/com/nayakfamily/familytree/domain/AuthorityAsserts.java create mode 100644 src/test/java/com/nayakfamily/familytree/domain/AuthorityTest.java create mode 100644 src/test/java/com/nayakfamily/familytree/domain/AuthorityTestSamples.java create mode 100644 src/test/java/com/nayakfamily/familytree/management/SecurityMetersServiceTests.java create mode 100644 src/test/java/com/nayakfamily/familytree/repository/timezone/DateTimeWrapper.java create mode 100644 src/test/java/com/nayakfamily/familytree/repository/timezone/DateTimeWrapperRepository.java create mode 100644 src/test/java/com/nayakfamily/familytree/security/DomainUserDetailsServiceIT.java create mode 100644 src/test/java/com/nayakfamily/familytree/security/SecurityUtilsUnitTest.java create mode 100644 src/test/java/com/nayakfamily/familytree/security/jwt/AuthenticationIntegrationTest.java create mode 100644 src/test/java/com/nayakfamily/familytree/security/jwt/JwtAuthenticationTestUtils.java create mode 100644 src/test/java/com/nayakfamily/familytree/security/jwt/TokenAuthenticationIT.java create mode 100644 src/test/java/com/nayakfamily/familytree/security/jwt/TokenAuthenticationSecurityMetersIT.java create mode 100644 src/test/java/com/nayakfamily/familytree/service/MailServiceIT.java create mode 100644 src/test/java/com/nayakfamily/familytree/service/UserServiceIT.java create mode 100644 src/test/java/com/nayakfamily/familytree/service/mapper/UserMapperTest.java create mode 100644 src/test/java/com/nayakfamily/familytree/web/filter/SpaWebFilterIT.java create mode 100644 src/test/java/com/nayakfamily/familytree/web/rest/AccountResourceIT.java create mode 100644 src/test/java/com/nayakfamily/familytree/web/rest/AuthenticateControllerIT.java create mode 100644 src/test/java/com/nayakfamily/familytree/web/rest/AuthorityResourceIT.java create mode 100644 src/test/java/com/nayakfamily/familytree/web/rest/PublicUserResourceIT.java create mode 100644 src/test/java/com/nayakfamily/familytree/web/rest/TestUtil.java create mode 100644 src/test/java/com/nayakfamily/familytree/web/rest/UserResourceIT.java create mode 100644 src/test/java/com/nayakfamily/familytree/web/rest/WithUnauthenticatedMockUser.java create mode 100644 src/test/java/com/nayakfamily/familytree/web/rest/errors/ExceptionTranslatorIT.java create mode 100644 src/test/java/com/nayakfamily/familytree/web/rest/errors/ExceptionTranslatorTestController.java create mode 100644 src/test/resources/META-INF/spring.factories create mode 100644 src/test/resources/config/application-testdev.yml create mode 100644 src/test/resources/config/application-testprod.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/junit-platform.properties create mode 100644 src/test/resources/logback.xml create mode 100644 src/test/resources/templates/mail/activationEmail.html create mode 100644 src/test/resources/templates/mail/creationEmail.html create mode 100644 src/test/resources/templates/mail/passwordResetEmail.html 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/environment.js 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/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..a914fd2 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,25 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/java/.devcontainer/base.Dockerfile + +# [Choice] Java version (use -bullseye variants on local arm64/Apple Silicon): 17, 17-bullseye, 17-buster +ARG VARIANT="17" +FROM mcr.microsoft.com/vscode/devcontainers/java:0-${VARIANT} + +# [Option] Install Maven +ARG INSTALL_MAVEN="false" +ARG MAVEN_VERSION="" +# [Option] Install Gradle +ARG INSTALL_GRADLE="false" +ARG GRADLE_VERSION="" +RUN if [ "${INSTALL_MAVEN}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install maven \"${MAVEN_VERSION}\""; fi \ + && if [ "${INSTALL_GRADLE}" = "true" ]; then su vscode -c "umask 0002 && . /usr/local/sdkman/bin/sdkman-init.sh && sdk install gradle \"${GRADLE_VERSION}\""; fi + +# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 +ARG NODE_VERSION="none" +RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment this line to install global node packages. +# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..985647e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,52 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.209.6/containers/java +{ + "name": "NayaksFamily", + "build": { + "dockerfile": "Dockerfile", + "args": { + // Update the VARIANT arg to pick a Java version: 17, 19 + // Append -bullseye or -buster to pin to an OS version. + // Use the -bullseye variants on local arm64/Apple Silicon. + "VARIANT": "17-bullseye", + // Options + // maven and gradle wrappers are used by default, we don't need them installed globally + // "INSTALL_MAVEN": "true", + // "INSTALL_GRADLE": "false", + "NODE_VERSION": "20.12.1" + } + }, + + "customizations": { + "vscode": { + // Set *default* container specific settings.json values on container create. + "settings": { + "java.jdt.ls.java.home": "/docker-java-home" + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "christian-kohler.npm-intellisense", + "firsttris.vscode-jest-runner", + "ms-vscode.vscode-typescript-tslint-plugin", + "dbaeumer.vscode-eslint", + "vscjava.vscode-java-pack", + "pivotal.vscode-boot-dev-pack", + "esbenp.prettier-vscode" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [9060, 3001, 9000, 8080], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "java -version", + + // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "features": { + "docker-in-docker": "latest", + "docker-from-docker": "latest" + } +} 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..4997c23 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,10 @@ +node_modules/ +src/main/docker/ +jest.conf.js +webpack/ +target/ +build/ +node/ +coverage/ +postcss.config.js +target/classes/static/ diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..cc67dd9 --- /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", "./tsconfig.test.json"] + }, + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + "@typescript-eslint/member-ordering": [ + "error", + { + "default": ["static-field", "instance-field", "constructor", "static-method", "instance-method"] + } + ], + "@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-argument": "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..d279ca6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,153 @@ +###################### +# 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 + +# 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/ +/buildSrc/.gradle/ +/buildSrc/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 + +###################### +# Code coverage +###################### +/coverage/ +/.nyc_output/ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..adefefb --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + + +"$(dirname "$0")/../npmw" exec --no-install lint-staged diff --git a/.lintstagedrc.cjs b/.lintstagedrc.cjs new file mode 100644 index 0000000..0531990 --- /dev/null +++ b/.lintstagedrc.cjs @@ -0,0 +1,3 @@ +module.exports = { + '{,**/}*.{md,json,yml,html,cjs,mjs,js,ts,tsx,css,scss,java}': ['prettier --write'], +}; diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1 @@ + diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..346d645 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..fec4ac0 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +node_modules +package-lock.json +.git +target/ + +# Generated by jhipster:client +target/classes/static/ + +# Generated by jhipster:maven +target +.mvn diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8550cbf --- /dev/null +++ b/.prettierrc @@ -0,0 +1,22 @@ +# Prettier configuration + +printWidth: 140 +singleQuote: true +tabWidth: 2 +useTabs: false + +# js and ts rules: +arrowParens: avoid + +# jsx and tsx rules: +bracketSameLine: false + +plugins: + - prettier-plugin-packagejson + - prettier-plugin-java + +# java rules: +overrides: + - files: "*.java" + options: + tabWidth: 4 diff --git a/.yo-rc.json b/.yo-rc.json new file mode 100644 index 0000000..4cd8d0b --- /dev/null +++ b/.yo-rc.json @@ -0,0 +1,40 @@ +{ + "generator-jhipster": { + "applicationType": "monolith", + "authenticationType": "jwt", + "baseName": "nayaksFamily", + "blueprints": [], + "buildTool": "maven", + "cacheProvider": "ehcache", + "clientFramework": "react", + "clientPackageManager": "npm", + "creationTimestamp": 1712758731366, + "databaseType": "sql", + "devDatabaseType": "h2Disk", + "devServerPort": 9060, + "enableHibernateCache": true, + "enableSwaggerCodegen": false, + "enableTranslation": false, + "entities": [], + "gitCompany": "", + "jhiPrefix": "jhi", + "jhipsterVersion": "8.3.0", + "jwtSecretKey": "YmVlMWZiNDdiNzQwN2Q4YTMyNjBhZTU3ZjQ1YjJkMDUyMDBiMGRiYzQ2ZDc1ZmMxNWQzYTA3MzYyNWE3ZWExOWM0YjJhMDQ4NmQ4ZmUyNTA2ZThkYjI2MWE4YTMwMjBjZjI2NjMwNzE1MGRiNGQ2ZDEwNmYzN2U4NmFmZDQwNmU=", + "languages": ["en"], + "messageBroker": false, + "nativeLanguage": "en", + "packageFolder": "com/nayakfamily/familytree", + "packageName": "com.nayakfamily.familytree", + "prodDatabaseType": "postgresql", + "searchEngine": false, + "serverPort": 8080, + "serviceDiscoveryType": false, + "testFrameworks": [], + "useSass": true, + "websocket": false, + "withAdminUi": true + }, + "git-company": "jgdvarun60", + "git-provider": "GitHub", + "repository-name": "nayaks-family" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..479c0f5 --- /dev/null +++ b/README.md @@ -0,0 +1,237 @@ +# nayaksFamily + +This application was generated using JHipster 8.3.0, you can find documentation and help at [https://www.jhipster.tech/documentation-archive/v8.3.0](https://www.jhipster.tech/documentation-archive/v8.3.0). + +## Project Structure + +Node is required for generation and recommended for development. `package.json` is always generated for a better development experience with prettier, commit hooks, scripts and so on. + +In the project root, JHipster generates configuration files for tools like git, prettier, eslint, husky, and others that are well known and you can find references in the web. + +`/src/*` structure follows default Java structure. + +- `.yo-rc.json` - Yeoman configuration file + JHipster configuration is stored in this file at `generator-jhipster` key. You may find `generator-jhipster-*` for specific blueprints configuration. +- `.yo-resolve` (optional) - Yeoman conflict resolver + Allows to use a specific action when conflicts are found skipping prompts for files that matches a pattern. Each line should match `[pattern] [action]` with pattern been a [Minimatch](https://github.com/isaacs/minimatch#minimatch) pattern and action been one of skip (default if omitted) or force. Lines starting with `#` are considered comments and are ignored. +- `.jhipster/*.json` - JHipster entity configuration files + +- `npmw` - wrapper to use locally installed npm. + JHipster installs Node and npm locally using the build tool by default. This wrapper makes sure npm is installed locally and uses it avoiding some differences different versions can cause. By using `./npmw` instead of the traditional `npm` you can configure a Node-less environment to develop or test your application. +- `/src/main/docker` - Docker configurations for the application and services that the application depends on + +## Development + +Before you can build this project, you must install and configure the following dependencies on your machine: + +1. [Node.js](https://nodejs.org/): 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. + +``` +./mvnw +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 nayaksFamily application for production, run: + +``` +./mvnw -Pprod clean verify +``` + +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 target/*.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: + +``` +./mvnw -Pprod,war clean verify +``` + +### JHipster Control Center + +JHipster Control Center can help you manage and control your application(s). You can start a local control center server (accessible on http://localhost:7419) with: + +``` +docker compose -f src/main/docker/jhipster-control-center.yml up +``` + +## Testing + +### 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 +``` + +### Spring Boot tests + +To launch your application's tests, run: + +``` +./mvnw verify +``` + +## Others + +### Code quality using Sonar + +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 forced authentication redirect for UI 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 maven plugin. + +Then, run a Sonar analysis: + +``` +./mvnw -Pprod clean verify sonar:sonar -Dsonar.login=admin -Dsonar.password=admin +``` + +If you need to re-run the Sonar phase, please be sure to specify at least the `initialize` phase since Sonar properties are loaded from the sonar-project.properties file. + +``` +./mvnw initialize sonar:sonar -Dsonar.login=admin -Dsonar.password=admin +``` + +Additionally, Instead of passing `sonar.password` and `sonar.login` as CLI arguments, these parameters can be configured from [sonar-project.properties](sonar-project.properties) as shown below: + +``` +sonar.login=admin +sonar.password=admin +``` + +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 postgresql database in a docker container, run: + +``` +docker compose -f src/main/docker/postgresql.yml up -d +``` + +To stop it and remove the container, run: + +``` +docker compose -f src/main/docker/postgresql.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: + +``` +npm run java:docker +``` + +Or build a arm64 docker image when using an arm64 processor os like MacOS with M1 processor family running: + +``` +npm run java:docker:arm64 +``` + +Then run: + +``` +docker compose -f src/main/docker/app.yml up -d +``` + +When running Docker Desktop on MacOS Big Sur or later, consider enabling experimental `Use the new Virtualization framework` for better processing performance ([disk access performance is worse](https://github.com/docker/roadmap/issues/7)). + +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 8.3.0 archive]: https://www.jhipster.tech/documentation-archive/v8.3.0 +[Using JHipster in development]: https://www.jhipster.tech/documentation-archive/v8.3.0/development/ +[Using Docker and Docker-Compose]: https://www.jhipster.tech/documentation-archive/v8.3.0/docker-compose +[Using JHipster in production]: https://www.jhipster.tech/documentation-archive/v8.3.0/production/ +[Running tests page]: https://www.jhipster.tech/documentation-archive/v8.3.0/running-tests/ +[Code quality page]: https://www.jhipster.tech/documentation-archive/v8.3.0/code-quality/ +[Setting up Continuous Integration]: https://www.jhipster.tech/documentation-archive/v8.3.0/setting-up-ci/ +[Node.js]: https://nodejs.org/ +[NPM]: https://www.npmjs.com/ +[Webpack]: https://webpack.github.io/ +[BrowserSync]: https://www.browsersync.io/ +[Jest]: https://facebook.github.io/jest/ +[Leaflet]: https://leafletjs.com/ +[DefinitelyTyped]: https://definitelytyped.org/ diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..5d5ae65 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + diff --git a/jest.conf.js b/jest.conf.js new file mode 100644 index 0000000..bcffae3 --- /dev/null +++ b/jest.conf.js @@ -0,0 +1,67 @@ +const tsconfig = require('./tsconfig.test.json'); + +module.exports = { + testEnvironment: 'jsdom', + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + tsconfig: './tsconfig.test.json', + compiler: 'typescript', + diagnostics: false, + }, + ], + }, + testEnvironmentOptions: { + url: 'http://localhost/', + }, + cacheDirectory: '/target/jest-cache', + coverageDirectory: '/target/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', + sinon: require.resolve('sinon/pkg/sinon.js'), + }), + reporters: [ + 'default', + ['jest-junit', { outputDirectory: './target/test-results/', outputName: 'TESTS-results-jest.xml' }], + ['jest-sonar', { outputDirectory: './target/test-results/jest', outputName: 'TESTS-results-sonar.xml' }], + ], + testPathIgnorePatterns: ['/node_modules/'], + setupFiles: ['/src/main/webapp/app/setup-tests.ts'], + globals: { + ...require('./webpack/environment'), + DEVELOPMENT: 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/mvnw b/mvnw new file mode 100755 index 0000000..8d937f4 --- /dev/null +++ b/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..c4586b5 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/npmw b/npmw new file mode 100755 index 0000000..785f188 --- /dev/null +++ b/npmw @@ -0,0 +1,42 @@ +#!/bin/sh + +basedir=`dirname "$0"` + +if [ -f "$basedir/mvnw" ]; then + bindir="$basedir/target/node" + repodir="$basedir/target/node/node_modules" + installCommand="$basedir/mvnw --batch-mode -ntp -Pwebapp frontend:install-node-and-npm@install-node-and-npm" + + PATH="$basedir/$builddir/:$PATH" + NPM_EXE="$basedir/$builddir/node_modules/npm/bin/npm-cli.js" + NODE_EXE="$basedir/$builddir/node" +elif [ -f "$basedir/gradlew" ]; then + bindir="$basedir/build/node/bin" + repodir="$basedir/build/node/lib/node_modules" + installCommand="$basedir/gradlew npmSetup" +else + echo "Using npm installed globally" + exec npm "$@" +fi + +NPM_EXE="$repodir/npm/bin/npm-cli.js" +NODE_EXE="$bindir/node" + +if [ ! -x "$NPM_EXE" ] || [ ! -x "$NODE_EXE" ]; then + $installCommand || true +fi + +if [ -x "$NODE_EXE" ]; then + echo "Using node installed locally $($NODE_EXE --version)" + PATH="$bindir:$PATH" +else + NODE_EXE='node' +fi + +if [ ! -x "$NPM_EXE" ]; then + echo "Local npm not found, using npm installed globally" + npm "$@" +else + echo "Using npm installed locally $($NODE_EXE $NPM_EXE --version)" + $NODE_EXE $NPM_EXE "$@" +fi diff --git a/npmw.cmd b/npmw.cmd new file mode 100644 index 0000000..b6e7980 --- /dev/null +++ b/npmw.cmd @@ -0,0 +1,31 @@ +@echo off + +setlocal + +set NPMW_DIR=%~dp0 + +if exist "%NPMW_DIR%mvnw.cmd" ( + set NODE_EXE=^"^" + set NODE_PATH=%NPMW_DIR%target\node\ + set NPM_EXE=^"%NPMW_DIR%target\node\npm.cmd^" + set INSTALL_NPM_COMMAND=^"%NPMW_DIR%mvnw.cmd^" -Pwebapp frontend:install-node-and-npm@install-node-and-npm +) else ( + set NODE_EXE=^"%NPMW_DIR%build\node\bin\node.exe^" + set NODE_PATH=%NPMW_DIR%build\node\bin\ + set NPM_EXE=^"%NPMW_DIR%build\node\lib\node_modules\npm\bin\npm-cli.js^" + set INSTALL_NPM_COMMAND=^"%NPMW_DIR%gradlew.bat^" npmSetup +) + +if not exist %NPM_EXE% ( + call %INSTALL_NPM_COMMAND% +) + +if exist %NODE_EXE% ( + Rem execute local npm with local node, whilst adding local node location to the PATH for this CMD session + endlocal & echo "%PATH%"|find /i "%NODE_PATH%;">nul || set "PATH=%NODE_PATH%;%PATH%" & call %NODE_EXE% %NPM_EXE% %* +) else if exist %NPM_EXE% ( + Rem execute local npm, whilst adding local npm location to the PATH for this CMD session + endlocal & echo "%PATH%"|find /i "%NODE_PATH%;">nul || set "PATH=%NODE_PATH%;%PATH%" & call %NPM_EXE% %* +) else ( + call npm %* +) diff --git a/package.json b/package.json new file mode 100644 index 0000000..867bb80 --- /dev/null +++ b/package.json @@ -0,0 +1,168 @@ +{ + "name": "nayaks-family", + "version": "0.0.1-SNAPSHOT", + "private": true, + "description": "Description for Nayaks Family", + "license": "UNLICENSED", + "scripts": { + "app:start": "./mvnw", + "app:up": "docker compose -f src/main/docker/app.yml up --wait", + "backend:build-cache": "./mvnw dependency:go-offline -ntp", + "backend:debug": "./mvnw -Dspring-boot.run.jvmArguments=\"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000\"", + "backend:doc:test": "./mvnw -ntp javadoc:javadoc --batch-mode", + "backend:info": "./mvnw --version", + "backend:nohttp:test": "./mvnw -ntp checkstyle:check --batch-mode", + "backend:start": "./mvnw -Dskip.installnodenpm -Dskip.npm", + "backend:unit:test": "./mvnw -ntp -Dskip.installnodenpm -Dskip.npm verify --batch-mode -Dlogging.level.ROOT=OFF -Dlogging.level.tech.jhipster=OFF -Dlogging.level.com.nayakfamily.familytree=OFF -Dlogging.level.org.springframework=OFF -Dlogging.level.org.springframework.web=OFF -Dlogging.level.org.springframework.security=OFF", + "build": "npm run webapp:prod --", + "build-watch": "concurrently 'npm run webapp:build:dev -- --watch' npm:backend:start", + "ci:backend:test": "npm run backend:info && npm run backend:doc:test && npm run backend:nohttp:test && npm run backend:unit:test -- -P$npm_package_config_default_environment", + "ci:e2e:package": "npm run java:$npm_package_config_packaging:$npm_package_config_default_environment -- -Pe2e -Denforcer.skip=true", + "ci:e2e:prepare": "npm run ci:e2e:prepare:docker", + "ci:e2e:prepare:docker": "npm run services:up --if-present && docker ps -a", + "preci:e2e:server:start": "npm run services:db:await --if-present && npm run services:others:await --if-present", + "ci:e2e:server:start": "java -jar target/e2e.$npm_package_config_packaging --spring.profiles.active=e2e,$npm_package_config_default_environment -Dlogging.level.ROOT=OFF -Dlogging.level.tech.jhipster=OFF -Dlogging.level.com.nayakfamily.familytree=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:e2e:teardown": "npm run ci:e2e:teardown:docker --if-present", + "ci:e2e:teardown:docker": "docker compose -f src/main/docker/services.yml down -v && docker ps -a", + "ci:frontend:test": "npm run webapp:build:$npm_package_config_default_environment && npm run test-ci", + "clean-www": "rimraf target/classes/static/", + "cleanup": "rimraf target/", + "docker:db:down": "docker compose -f src/main/docker/postgresql.yml down -v", + "docker:db:up": "docker compose -f src/main/docker/postgresql.yml up --wait", + "java:docker": "./mvnw -ntp verify -DskipTests -Pprod jib:dockerBuild", + "java:docker:arm64": "npm run java:docker -- -Djib-maven-plugin.architecture=arm64", + "java:docker:dev": "npm run java:docker -- -Pdev,webapp", + "java:docker:prod": "npm run java:docker -- -Pprod", + "java:jar": "./mvnw -ntp verify -DskipTests --batch-mode", + "java:jar:dev": "npm run java:jar -- -Pdev,webapp", + "java:jar:prod": "npm run java:jar -- -Pprod", + "java:war": "./mvnw -ntp verify -DskipTests --batch-mode -Pwar", + "java:war:dev": "npm run java:war -- -Pdev,webapp", + "java:war:prod": "npm run java:war -- -Pprod", + "jest": "jest --coverage --logHeapUsage --maxWorkers=2 --config jest.conf.js", + "jest:update": "npm run jest -- --updateSnapshot", + "lint": "eslint . --ext .js,.ts,.jsx,.tsx", + "lint:fix": "npm run lint -- --fix", + "prepare": "husky", + "prettier:check": "prettier --check \"{,src/**/,webpack/,.blueprint/**/}*.{md,json,yml,html,cjs,mjs,js,ts,tsx,css,scss,java}\"", + "prettier:format": "prettier --write \"{,src/**/,webpack/,.blueprint/**/}*.{md,json,yml,html,cjs,mjs,js,ts,tsx,css,scss,java}\"", + "services:up": "docker compose -f src/main/docker/services.yml up --wait", + "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", + "watch": "concurrently 'npm run start' npm:backend:start", + "webapp:build": "npm run clean-www && npm run webapp:build:dev --", + "webapp:build:dev": "webpack --config webpack/webpack.dev.js --env stats=minimal", + "webapp:build:prod": "webpack --config webpack/webpack.prod.js --progress=profile", + "webapp:dev": "npm run webpack-dev-server -- --config webpack/webpack.dev.js --env stats=minimal", + "webapp:dev-verbose": "npm run webpack-dev-server -- --config webpack/webpack.dev.js --progress=profile --env stats=normal", + "webapp:prod": "npm run clean-www && npm run webapp:build:prod --", + "webapp:test": "npm run test --", + "webpack-dev-server": "webpack serve" + }, + "config": { + "backend_port": 8080, + "default_environment": "prod", + "packaging": "jar" + }, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "6.5.2", + "@fortawesome/free-solid-svg-icons": "6.5.2", + "@fortawesome/react-fontawesome": "0.2.0", + "@reduxjs/toolkit": "2.2.3", + "axios": "1.6.8", + "bootstrap": "5.3.3", + "dayjs": "1.11.10", + "lodash": "4.17.21", + "path-browserify": "1.0.1", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-hook-form": "7.51.2", + "react-jhipster": "0.25.3", + "react-loadable": "5.5.0", + "react-redux": "9.1.0", + "react-redux-loading-bar": "5.0.8", + "react-router": "6.22.3", + "react-router-dom": "6.22.3", + "react-toastify": "10.0.5", + "react-transition-group": "4.4.5", + "reactstrap": "9.2.2", + "redux": "5.0.1", + "tslib": "2.6.2", + "uuid": "9.0.1" + }, + "devDependencies": { + "@testing-library/react": "14.2.2", + "@types/jest": "29.5.12", + "@types/lodash": "4.17.0", + "@types/node": "20.11.25", + "@types/react": "18.2.74", + "@types/react-dom": "18.2.24", + "@types/react-redux": "7.1.33", + "@types/redux": "3.6.31", + "@types/webpack-env": "1.18.4", + "@typescript-eslint/eslint-plugin": "7.5.0", + "@typescript-eslint/parser": "7.5.0", + "autoprefixer": "10.4.19", + "browser-sync": "2.29.3", + "browser-sync-webpack-plugin": "2.3.0", + "concurrently": "8.2.2", + "copy-webpack-plugin": "12.0.2", + "core-js": "3.36.1", + "cross-env": "7.0.3", + "css-loader": "7.0.0", + "css-minimizer-webpack-plugin": "6.0.0", + "eslint": "8.57.0", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-react": "7.34.1", + "eslint-webpack-plugin": "4.1.0", + "fork-ts-checker-webpack-plugin": "9.0.2", + "generator-jhipster": "8.3.0", + "html-webpack-plugin": "5.6.0", + "husky": "9.0.11", + "identity-obj-proxy": "3.0.0", + "jest": "29.7.0", + "jest-environment-jsdom": "29.7.0", + "jest-junit": "16.0.0", + "jest-sonar": "0.2.16", + "json-loader": "0.5.7", + "lint-staged": "15.2.2", + "mini-css-extract-plugin": "2.8.1", + "postcss-loader": "8.1.1", + "prettier": "3.2.5", + "prettier-plugin-java": "2.6.0", + "prettier-plugin-packagejson": "2.4.14", + "react-infinite-scroll-component": "6.1.0", + "redux-mock-store": "1.5.4", + "rimraf": "5.0.5", + "sass": "1.74.1", + "sass-loader": "14.1.1", + "simple-progress-webpack-plugin": "2.0.0", + "sinon": "17.0.1", + "source-map-loader": "5.0.0", + "sourcemap-istanbul-instrumenter-loader": "0.2.0", + "style-loader": "3.3.4", + "swagger-ui-dist": "5.13.0", + "terser-webpack-plugin": "5.3.10", + "thread-loader": "4.0.2", + "ts-jest": "29.1.2", + "ts-loader": "9.5.1", + "typescript": "5.4.4", + "wait-on": "7.2.0", + "webpack": "5.91.0", + "webpack-cli": "5.1.4", + "webpack-dev-server": "5.0.4", + "webpack-merge": "5.10.0", + "webpack-notifier": "1.15.0", + "workbox-webpack-plugin": "7.0.0" + }, + "engines": { + "node": ">=20.12.1" + }, + "cacheDirectories": [ + "node_modules" + ] +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..17899e1 --- /dev/null +++ b/pom.xml @@ -0,0 +1,1194 @@ + + + 4.0.0 + + com.nayakfamily.familytree + nayaks-family + 0.0.1-SNAPSHOT + jar + Nayaks Family + Description for Nayaks Family + + org.springframework.boot + spring-boot-starter-parent + 3.2.4 + + + + + + + 3.2.5 + 17 + v20.12.1 + 10.5.1 + UTF-8 + UTF-8 + yyyyMMddHHmmss + ${java.version} + ${java.version} + com.nayakfamily.familytree.NayaksFamilyApp + -Djava.security.egd=file:/dev/./urandom -Xmx1G + jdt_apt + false + ${project.parent.version} + 1.2.1 + 10.15.0 + 1.11 + 1.15.0 + 8.0.2 + 0.8.12 + 8.3.0 + amd64 + eclipse-temurin:17-jre-focal + 3.4.2 + 1.0.0 + + + + + + 1.5.5.Final + 3.1.0 + 3.3.1 + 3.3.2 + 3.13.0 + 3.4.1 + 3.2.5 + 3.3.0 + 3.6.3 + 3.3.1 + 3.12.1 + 3.2.5 + 3.4.0 + 2.8.0 + 0.0.11 + + + + + + 1.2.1 + 3.11.0.3922 + 2.43.0 + 2.5.0 + + + + + tech.jhipster + jhipster-framework + ${jhipster-framework.version} + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + org.springframework.boot + spring-boot-loader-tools + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-aop + + + org.springframework.boot + spring-boot-starter-cache + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-logging + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-undertow + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.boot + spring-boot-test + test + + + org.springframework.security + spring-security-data + + + org.springframework.security + spring-security-test + test + + + org.springdoc + springdoc-openapi-starter-webmvc-api + ${springdoc-openapi-starter-webmvc-api.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate6 + + + com.fasterxml.jackson.datatype + jackson-datatype-hppc + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.module + jackson-module-jaxb-annotations + + + com.tngtech.archunit + archunit-junit5-api + ${archunit-junit5.version} + test + + + + + com.tngtech.archunit + archunit-junit5-engine + ${archunit-junit5.version} + test + + + com.zaxxer + HikariCP + + + io.dropwizard.metrics + metrics-core + + + io.micrometer + micrometer-registry-prometheus + + + jakarta.annotation + jakarta.annotation-api + + + javax.cache + cache-api + + + org.apache.commons + commons-lang3 + + + org.ehcache + ehcache + jakarta + + + org.glassfish.jaxb + jaxb-runtime + provided + + + org.hibernate.orm + hibernate-core + + + org.hibernate.orm + hibernate-jcache + + + org.hibernate.orm + hibernate-jpamodelgen + provided + + + org.hibernate.validator + hibernate-validator + + + org.liquibase + liquibase-core + ${liquibase.version} + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + provided + + + org.testcontainers + jdbc + test + + + org.testcontainers + junit-jupiter + test + + + org.testcontainers + postgresql + test + + + org.testcontainers + testcontainers + test + + + + + spring-boot:run + + + org.springframework.boot + spring-boot-maven-plugin + + + com.diffplug.spotless + spotless-maven-plugin + + + com.google.cloud.tools + jib-maven-plugin + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-enforcer-plugin + + + org.apache.maven.plugins + maven-failsafe-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.apache.maven.plugins + maven-resources-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.codehaus.mojo + properties-maven-plugin + + + org.gaul + modernizer-maven-plugin + + + org.jacoco + jacoco-maven-plugin + + + org.sonarsource.scanner.maven + sonar-maven-plugin + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + ${start-class} + + + + + com.diffplug.spotless + spotless-maven-plugin + ${spotless-maven-plugin.version} + + + + + + + + spotless + process-sources + + apply + + + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin.version} + + target + ${node.version} + ${npm.version} + + + + com.google.cloud.tools + jib-maven-plugin + ${jib-maven-plugin.version} + + + ${jib-maven-plugin.image} + + + ${jib-maven-plugin.architecture} + linux + + + + + nayaksfamily:latest + + + + bash + + /entrypoint.sh + + + 8080 + + + ALWAYS + 0 + + USE_CURRENT_TIMESTAMP + 1000 + + + src/main/docker/jib + + + /entrypoint.sh + 755 + + + + + + + io.github.git-commit-id + git-commit-id-maven-plugin + ${git-commit-id-maven-plugin.version} + + + + revision + + + + + false + false + true + + ^git.commit.id.abbrev$ + ^git.commit.id.describe$ + ^git.branch$ + + + + + net.nicoulaj.maven.plugins + checksum-maven-plugin + ${checksum-maven-plugin.version} + + + org.apache.maven.plugins + maven-antrun-plugin + ${maven-antrun-plugin.version} + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven-checkstyle-plugin.version} + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + io.spring.nohttp + nohttp-checkstyle + ${nohttp-checkstyle.version} + + + + checkstyle.xml + pom.xml,README.md + .git/**/*,target/**/*,node_modules/**/* + ./ + + + + + check + + + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + ${java.version} + ${java.version} + true + + + org.springframework.boot + spring-boot-configuration-processor + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.hibernate.orm + hibernate-jpamodelgen + + + org.glassfish.jaxb + jaxb-runtime + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + enforce-versions + + enforce + + + + enforce-dependencyConvergence + + + + + false + + + enforce + + + + + + + You are running an older version of Maven. JHipster requires at least Maven ${maven.version} + [${maven.version},) + + + You are running an incompatible version of Java. JHipster supports JDK 17 to 21. + [17,18),[18,19),[19,20),[20,21),[21,22) + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + + + + ${project.build.outputDirectory} + alphabetical + + **/*IT* + **/*IntTest* + + @{argLine} -Dspring.profiles.active=${profile.test} + + + + integration-test + + integration-test + + + + verify + + verify + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + ${maven.compiler.source} + + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + config-resources + validate + + copy-resources + + + ${project.build.directory}/classes + false + + # + + + + src/main/resources/ + true + + config/*.yml + + + + + + + + + org.apache.maven.plugins + maven-site-plugin + ${maven-site-plugin.version} + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + alphabetical + + **/*IT* + **/*IntTest* + + + src/test/resources/logback.xml + + + + + org.apache.maven.plugins + maven-war-plugin + ${maven-war-plugin.version} + + + default-war + + war + + package + + + + WEB-INF/**,META-INF/** + false + target/classes/static/ + + + src/main/webapp + + WEB-INF/** + + + + + + + org.codehaus.mojo + properties-maven-plugin + ${properties-maven-plugin.version} + + + initialize + + read-project-properties + + + + sonar-project.properties + + + + + + + org.gaul + modernizer-maven-plugin + ${modernizer-maven-plugin.version} + + + modernizer + package + + modernizer + + + + + ${java.version} + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + pre-unit-tests + + prepare-agent + + + + + post-unit-test + test + + report + + + + pre-integration-tests + + prepare-agent-integration + + + + + post-integration-tests + post-integration-test + + report-integration + + + + + + org.liquibase + liquibase-maven-plugin + ${liquibase.version} + + config/liquibase/master.xml + ${project.basedir}/src/main/resources/config/liquibase/changelog/${maven.build.timestamp}_changelog.xml + ${liquibase-plugin.driver} + ${liquibase-plugin.url} + + ${liquibase-plugin.username} + ${liquibase-plugin.password} + hibernate:spring:com.nayakfamily.familytree.domain?dialect=${liquibase-plugin.hibernate-dialect}&hibernate.physical_naming_strategy=org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy&hibernate.implicit_naming_strategy=org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + true + debug + !test + + + + org.liquibase + liquibase-core + ${liquibase.version} + + + org.liquibase.ext + liquibase-hibernate6 + ${liquibase.version} + + + jakarta.validation + jakarta.validation-api + ${jakarta-validation.version} + + + org.springframework.boot + spring-boot-starter-data-jpa + ${spring-boot.version} + + + com.h2database + h2 + ${h2.version} + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + ${sonar-maven-plugin.version} + + + + + + + + api-docs + + ,api-docs + + + + dev + + true + + + + dev${profile.tls}${profile.no-liquibase} + testdev + org.hibernate.dialect.H2Dialect + org.h2.Driver + jdbc:h2:file:${project.build.directory}/h2db/db/nayaksFamily + nayaksFamily + + + + + org.springframework.boot + spring-boot-devtools + true + + + com.h2database + h2 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + com/nayakfamily/familytree/config/PostgreSqlTestContainer.java + + + + + + + + + eclipse + + + m2e.version + + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + + + + org.eclipse.m2e + lifecycle-mapping + ${lifecycle-mapping.version} + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + prepare-agent + + + + + + + + + com.github.eirslett + frontend-maven-plugin + ${frontend-maven-plugin.version} + + install-node-and-npm + npm + + + + + + + + + + + + + + + + + IDE + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.hibernate.orm + hibernate-jpamodelgen + + + + + no-liquibase + + ,no-liquibase + + + + prod + + + prod${profile.api-docs}${profile.tls}${profile.e2e}${profile.no-liquibase} + testprod + org.hibernate.dialect.PostgreSQLDialect + org.postgresql.Driver + jdbc:postgresql://localhost:5432/nayaksFamily + nayaksFamily + + + + + + org.apache.maven.plugins + maven-clean-plugin + + + + target/classes/static/ + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + build-info + + + + + + io.github.git-commit-id + git-commit-id-maven-plugin + + + com.github.eirslett + frontend-maven-plugin + + + install-node-and-npm + + install-node-and-npm + + + + npm install + + npm + + + + webapp build test + + npm + + test + + run webapp:test + false + + + + webapp build prod + + npm + + generate-resources + + run webapp:prod + + ${project.version} + + false + + + + + + + + + org.postgresql + postgresql + + + + + tls + + ,tls + + + + war + + + + org.apache.maven.plugins + maven-war-plugin + + + + + + webapp + + true + + + + dev${profile.no-liquibase} + + + + + net.nicoulaj.maven.plugins + checksum-maven-plugin + + + create-pre-compiled-webapp-checksum + + files + + generate-resources + + + create-compiled-webapp-checksum + + files + + compile + + checksums.csv.old + + + + + + + ${project.basedir} + + src/main/webapp/**/*.* + target/classes/static/**/*.* + package-lock.json + package.json + tsconfig.json + .postcss.config.js + webpack/*.* + + + **/app/**/service-worker.js + **/app/**/vendor.css + + + + false + false + false + + SHA-1 + + true + true + + + + org.apache.maven.plugins + maven-antrun-plugin + + + eval-frontend-checksum + generate-resources + + run + + + + + + + + + + + + true + + + + + + com.github.eirslett + frontend-maven-plugin + + + install-node-and-npm + + install-node-and-npm + + + + npm install + + npm + + + + webapp build dev + + npm + + generate-resources + + run webapp:build + + ${project.version} + + false + + + + + + + + + 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/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..c7696a8 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,43 @@ +sonar.projectKey = nayaksFamily +sonar.projectName = nayaksFamily generated by jhipster + +# Typescript tests files must be inside sources and tests, otherwise `INFO: Test execution data ignored for 80 unknown files, including:` +# is shown. +sonar.sources = src +sonar.tests = src +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 = target/site/**/jacoco*.xml +sonar.java.codeCoveragePlugin = jacoco +sonar.junit.reportPaths = target/surefire-reports,target/failsafe-reports +sonar.testExecutionReportPaths = target/test-results/jest/TESTS-results-sonar.xml +sonar.javascript.lcov.reportPaths = target/test-results/lcov.info + +sonar.sourceEncoding = UTF-8 +sonar.exclusions = src/main/webapp/content/**/*.*, src/main/webapp/i18n/*.js, target/classes/static/**/*.* + +sonar.issue.ignore.multicriteria = S1192,S125,S3437,S4502,S4684,S5145,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-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 = java: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 +# Rule https://rules.sonarsource.com/java/RSPEC-5145 log filter is applied +sonar.issue.ignore.multicriteria.S5145.resourceKey = src/main/java/**/* +sonar.issue.ignore.multicriteria.S5145.ruleKey = javasecurity:S5145 +# 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-1192 +sonar.issue.ignore.multicriteria.S1192.resourceKey = src/main/java/**/CacheConfiguration.java +sonar.issue.ignore.multicriteria.S1192.ruleKey = java:S1192 +# Rule https://rules.sonarsource.com/xml/RSPEC-125 +sonar.issue.ignore.multicriteria.S125.resourceKey = src/main/resources/logback-spring.xml +sonar.issue.ignore.multicriteria.S125.ruleKey = xml:S125 diff --git a/src/main/docker/app.yml b/src/main/docker/app.yml new file mode 100644 index 0000000..255f6aa --- /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 +name: nayaksfamily +services: + app: + image: nayaksfamily + environment: + - _JAVA_OPTIONS=-Xmx512m -Xms256m + - SPRING_PROFILES_ACTIVE=prod,api-docs + - MANAGEMENT_PROMETHEUS_METRICS_EXPORT_ENABLED=true + - SPRING_DATASOURCE_URL=jdbc:postgresql://postgresql:5432/nayaksFamily + - SPRING_LIQUIBASE_URL=jdbc:postgresql://postgresql:5432/nayaksFamily + ports: + - 127.0.0.1:8080:8080 + healthcheck: + test: + - CMD + - curl + - -f + - http://localhost:8080/management/health + interval: 5s + timeout: 5s + retries: 40 + depends_on: + postgresql: + condition: service_healthy + postgresql: + extends: + file: ./postgresql.yml + service: postgresql 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..dbbb20f --- /dev/null +++ b/src/main/docker/jhipster-control-center.yml @@ -0,0 +1,51 @@ +## 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. + +name: nayaksfamily +services: + jhipster-control-center: + image: 'jhipster/jhipster-control-center:v0.5.0' + 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 + - 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=YmVlMWZiNDdiNzQwN2Q4YTMyNjBhZTU3ZjQ1YjJkMDUyMDBiMGRiYzQ2ZDc1ZmMxNWQzYTA3MzYyNWE3ZWExOWM0YjJhMDQ4NmQ4ZmUyNTA2ZThkYjI2MWE4YTMwMjBjZjI2NjMwNzE1MGRiNGQ2ZDEwNmYzN2U4NmFmZDQwNmU= + - SPRING_CLOUD_DISCOVERY_CLIENT_SIMPLE_INSTANCES_NAYAKSFAMILY_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..b023d04 --- /dev/null +++ b/src/main/docker/jib/entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +echo "The application will start in ${JHIPSTER_SLEEP}s..." && sleep ${JHIPSTER_SLEEP} + +# usage: file_env VAR [DEFAULT] +# ie: file_env 'XYZ_DB_PASSWORD' 'example' +# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of +# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature) +file_env() { + local var="$1" + local fileVar="${var}_FILE" + local def="${2:-}" + if [[ ${!var:-} && ${!fileVar:-} ]]; then + echo >&2 "error: both $var and $fileVar are set (but are exclusive)" + exit 1 + fi + local val="$def" + if [[ ${!var:-} ]]; then + val="${!var}" + elif [[ ${!fileVar:-} ]]; then + val="$(< "${!fileVar}")" + fi + + if [[ -n $val ]]; then + export "$var"="$val" + fi + + unset "$fileVar" +} + +file_env 'SPRING_DATASOURCE_URL' +file_env 'SPRING_DATASOURCE_USERNAME' +file_env 'SPRING_DATASOURCE_PASSWORD' +file_env 'SPRING_LIQUIBASE_URL' +file_env 'SPRING_LIQUIBASE_USER' +file_env 'SPRING_LIQUIBASE_PASSWORD' +file_env 'JHIPSTER_REGISTRY_PASSWORD' + +exec java ${JAVA_OPTS} -noverify -XX:+AlwaysPreTouch -Djava.security.egd=file:/dev/./urandom -cp /app/resources/:/app/classes/:/app/libs/* "com.nayakfamily.familytree.NayaksFamilyApp" "$@" diff --git a/src/main/docker/monitoring.yml b/src/main/docker/monitoring.yml new file mode 100644 index 0000000..dc4e40f --- /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 +name: nayaksfamily +services: + prometheus: + image: prom/prometheus:v2.51.1 + 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 + grafana: + image: grafana/grafana:10.4.1 + 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/postgresql.yml b/src/main/docker/postgresql.yml new file mode 100644 index 0000000..8517108 --- /dev/null +++ b/src/main/docker/postgresql.yml @@ -0,0 +1,20 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +name: nayaksfamily +services: + postgresql: + image: postgres:16.2 + # volumes: + # - ~/volumes/jhipster/nayaksFamily/postgresql/:/var/lib/postgresql/data/ + environment: + - POSTGRES_USER=nayaksFamily + - POSTGRES_PASSWORD= + - POSTGRES_HOST_AUTH_METHOD=trust + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER}'] + interval: 5s + timeout: 5s + retries: 10 + # If you want to expose these ports outside your dev PC, + # remove the "127.0.0.1:" prefix + ports: + - 127.0.0.1:5432:5432 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/services.yml b/src/main/docker/services.yml new file mode 100644 index 0000000..455a9bb --- /dev/null +++ b/src/main/docker/services.yml @@ -0,0 +1,7 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +name: nayaksfamily +services: + postgresql: + extends: + file: ./postgresql.yml + service: postgresql diff --git a/src/main/docker/sonar.yml b/src/main/docker/sonar.yml new file mode 100644 index 0000000..549291f --- /dev/null +++ b/src/main/docker/sonar.yml @@ -0,0 +1,15 @@ +# This configuration is intended for development purpose, it's **your** responsibility to harden it for production +name: nayaksfamily +services: + sonar: + container_name: sonarqube + image: sonarqube:10.4.1-community + # Forced authentication redirect for UI 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 + - 127.0.0.1:9000:9000 diff --git a/src/main/java/com/nayakfamily/familytree/ApplicationWebXml.java b/src/main/java/com/nayakfamily/familytree/ApplicationWebXml.java new file mode 100644 index 0000000..67b05e3 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/ApplicationWebXml.java @@ -0,0 +1,19 @@ +package com.nayakfamily.familytree; + +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(NayaksFamilyApp.class); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/GeneratedByJHipster.java b/src/main/java/com/nayakfamily/familytree/GeneratedByJHipster.java new file mode 100644 index 0000000..c1214c6 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/GeneratedByJHipster.java @@ -0,0 +1,13 @@ +package com.nayakfamily.familytree; + +import jakarta.annotation.Generated; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Generated(value = "JHipster", comments = "Generated by JHipster 8.3.0") +@Retention(RetentionPolicy.SOURCE) +@Target({ ElementType.TYPE }) +public @interface GeneratedByJHipster { +} diff --git a/src/main/java/com/nayakfamily/familytree/NayaksFamilyApp.java b/src/main/java/com/nayakfamily/familytree/NayaksFamilyApp.java new file mode 100644 index 0000000..d3755da --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/NayaksFamilyApp.java @@ -0,0 +1,108 @@ +package com.nayakfamily.familytree; + +import com.nayakfamily.familytree.config.ApplicationProperties; +import com.nayakfamily.familytree.config.CRLFLogConverter; +import jakarta.annotation.PostConstruct; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; +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 NayaksFamilyApp { + + private static final Logger log = LoggerFactory.getLogger(NayaksFamilyApp.class); + + private final Environment env; + + public NayaksFamilyApp(Environment env) { + this.env = env; + } + + /** + * Initializes nayaksFamily. + *

+ * 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(NayaksFamilyApp.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 applicationName = env.getProperty("spring.application.name"); + 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( + CRLFLogConverter.CRLF_SAFE_MARKER, + """ + + ---------------------------------------------------------- + \tApplication '{}' is running! Access URLs: + \tLocal: \t\t{}://localhost:{}{} + \tExternal: \t{}://{}:{}{} + \tProfile(s): \t{} + ----------------------------------------------------------""", + applicationName, + protocol, + serverPort, + contextPath, + protocol, + hostAddress, + serverPort, + contextPath, + env.getActiveProfiles().length == 0 ? env.getDefaultProfiles() : env.getActiveProfiles() + ); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/aop/logging/LoggingAspect.java b/src/main/java/com/nayakfamily/familytree/aop/logging/LoggingAspect.java new file mode 100644 index 0000000..192f7dc --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/aop/logging/LoggingAspect.java @@ -0,0 +1,113 @@ +package com.nayakfamily.familytree.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.nayakfamily.familytree.repository..*)" + + " || within(com.nayakfamily.familytree.service..*)" + + " || within(com.nayakfamily.familytree.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 ? String.valueOf(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/nayakfamily/familytree/aop/logging/package-info.java b/src/main/java/com/nayakfamily/familytree/aop/logging/package-info.java new file mode 100644 index 0000000..a4ffa54 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/aop/logging/package-info.java @@ -0,0 +1,4 @@ +/** + * Logging aspect. + */ +package com.nayakfamily.familytree.aop.logging; diff --git a/src/main/java/com/nayakfamily/familytree/config/ApplicationProperties.java b/src/main/java/com/nayakfamily/familytree/config/ApplicationProperties.java new file mode 100644 index 0000000..e5aa843 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/ApplicationProperties.java @@ -0,0 +1,37 @@ +package com.nayakfamily.familytree.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Properties specific to Nayaks Family. + *

+ * 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 { + + private final Liquibase liquibase = new Liquibase(); + + // jhipster-needle-application-properties-property + + public Liquibase getLiquibase() { + return liquibase; + } + + // jhipster-needle-application-properties-property-getter + + public static class Liquibase { + + private Boolean asyncStart; + + public Boolean getAsyncStart() { + return asyncStart; + } + + public void setAsyncStart(Boolean asyncStart) { + this.asyncStart = asyncStart; + } + } + // jhipster-needle-application-properties-property-class +} diff --git a/src/main/java/com/nayakfamily/familytree/config/AsyncConfiguration.java b/src/main/java/com/nayakfamily/familytree/config/AsyncConfiguration.java new file mode 100644 index 0000000..8bcdeb1 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/AsyncConfiguration.java @@ -0,0 +1,48 @@ +package com.nayakfamily.familytree.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.context.annotation.Profile; +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 +@Profile("!testdev & !testprod") +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/nayakfamily/familytree/config/CRLFLogConverter.java b/src/main/java/com/nayakfamily/familytree/config/CRLFLogConverter.java new file mode 100644 index 0000000..673ce85 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/CRLFLogConverter.java @@ -0,0 +1,69 @@ +package com.nayakfamily.familytree.config; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.pattern.CompositeConverter; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; +import org.springframework.boot.ansi.AnsiColor; +import org.springframework.boot.ansi.AnsiElement; +import org.springframework.boot.ansi.AnsiOutput; +import org.springframework.boot.ansi.AnsiStyle; + +/** + * Log filter to prevent attackers from forging log entries by submitting input containing CRLF characters. + * CRLF characters are replaced with a red colored _ character. + * + * @see Log Forging Description + * @see JHipster issue + */ +public class CRLFLogConverter extends CompositeConverter { + + public static final Marker CRLF_SAFE_MARKER = MarkerFactory.getMarker("CRLF_SAFE"); + + private static final String[] SAFE_LOGGERS = { + "org.hibernate", + "org.springframework.boot.autoconfigure", + "org.springframework.boot.diagnostics", + }; + private static final Map ELEMENTS; + + static { + Map ansiElements = new HashMap<>(); + ansiElements.put("faint", AnsiStyle.FAINT); + ansiElements.put("red", AnsiColor.RED); + ansiElements.put("green", AnsiColor.GREEN); + ansiElements.put("yellow", AnsiColor.YELLOW); + ansiElements.put("blue", AnsiColor.BLUE); + ansiElements.put("magenta", AnsiColor.MAGENTA); + ansiElements.put("cyan", AnsiColor.CYAN); + ELEMENTS = Collections.unmodifiableMap(ansiElements); + } + + @Override + protected String transform(ILoggingEvent event, String in) { + AnsiElement element = ELEMENTS.get(getFirstOption()); + List markers = event.getMarkerList(); + if ((markers != null && !markers.isEmpty() && markers.get(0).contains(CRLF_SAFE_MARKER)) || isLoggerSafe(event)) { + return in; + } + String replacement = element == null ? "_" : toAnsiString("_", element); + return in.replaceAll("[\n\r\t]", replacement); + } + + protected boolean isLoggerSafe(ILoggingEvent event) { + for (String safeLogger : SAFE_LOGGERS) { + if (event.getLoggerName().startsWith(safeLogger)) { + return true; + } + } + return false; + } + + protected String toAnsiString(String in, AnsiElement element) { + return AnsiOutput.toString(element, in); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/config/CacheConfiguration.java b/src/main/java/com/nayakfamily/familytree/config/CacheConfiguration.java new file mode 100644 index 0000000..a82a9d5 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/CacheConfiguration.java @@ -0,0 +1,80 @@ +package com.nayakfamily.familytree.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.nayakfamily.familytree.repository.UserRepository.USERS_BY_LOGIN_CACHE); + createCache(cm, com.nayakfamily.familytree.repository.UserRepository.USERS_BY_EMAIL_CACHE); + createCache(cm, com.nayakfamily.familytree.domain.User.class.getName()); + createCache(cm, com.nayakfamily.familytree.domain.Authority.class.getName()); + createCache(cm, com.nayakfamily.familytree.domain.User.class.getName() + ".authorities"); + // 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/nayakfamily/familytree/config/Constants.java b/src/main/java/com/nayakfamily/familytree/config/Constants.java new file mode 100644 index 0000000..c4d4816 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/Constants.java @@ -0,0 +1,15 @@ +package com.nayakfamily.familytree.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/nayakfamily/familytree/config/DatabaseConfiguration.java b/src/main/java/com/nayakfamily/familytree/config/DatabaseConfiguration.java new file mode 100644 index 0000000..ffc4ea3 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/DatabaseConfiguration.java @@ -0,0 +1,57 @@ +package com.nayakfamily.familytree.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.nayakfamily.familytree.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/nayakfamily/familytree/config/DateTimeFormatConfiguration.java b/src/main/java/com/nayakfamily/familytree/config/DateTimeFormatConfiguration.java new file mode 100644 index 0000000..637b079 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/DateTimeFormatConfiguration.java @@ -0,0 +1,20 @@ +package com.nayakfamily.familytree.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/nayakfamily/familytree/config/JacksonConfiguration.java b/src/main/java/com/nayakfamily/familytree/config/JacksonConfiguration.java new file mode 100644 index 0000000..75fc88b --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/JacksonConfiguration.java @@ -0,0 +1,34 @@ +package com.nayakfamily.familytree.config; + +import com.fasterxml.jackson.datatype.hibernate6.Hibernate6Module; +import com.fasterxml.jackson.datatype.hibernate6.Hibernate6Module.Feature; +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; + +@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 Hibernate6Module hibernate6Module() { + return new Hibernate6Module().configure(Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS, true); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/config/LiquibaseConfiguration.java b/src/main/java/com/nayakfamily/familytree/config/LiquibaseConfiguration.java new file mode 100644 index 0000000..ccc63ee --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/LiquibaseConfiguration.java @@ -0,0 +1,81 @@ +package com.nayakfamily.familytree.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.beans.factory.annotation.Value; +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; + } + + @Value("${application.liquibase.async-start:true}") + private Boolean asyncStart; + + @Bean + public SpringLiquibase liquibase( + @Qualifier("taskExecutor") Executor executor, + LiquibaseProperties liquibaseProperties, + @LiquibaseDataSource ObjectProvider liquibaseDataSource, + ObjectProvider dataSource, + DataSourceProperties dataSourceProperties + ) { + SpringLiquibase liquibase; + if (Boolean.TRUE.equals(asyncStart)) { + liquibase = SpringLiquibaseUtil.createAsyncSpringLiquibase( + this.env, + executor, + liquibaseDataSource.getIfAvailable(), + liquibaseProperties, + dataSource.getIfUnique(), + dataSourceProperties + ); + } else { + liquibase = SpringLiquibaseUtil.createSpringLiquibase( + 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.setLabelFilter(liquibaseProperties.getLabelFilter()); + 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/nayakfamily/familytree/config/LocaleConfiguration.java b/src/main/java/com/nayakfamily/familytree/config/LocaleConfiguration.java new file mode 100644 index 0000000..e45a8f7 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/LocaleConfiguration.java @@ -0,0 +1,24 @@ +package com.nayakfamily.familytree.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() { + return new AngularCookieLocaleResolver("NG_TRANSLATE_LANG_KEY"); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor(); + localeChangeInterceptor.setParamName("language"); + registry.addInterceptor(localeChangeInterceptor); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/config/LoggingAspectConfiguration.java b/src/main/java/com/nayakfamily/familytree/config/LoggingAspectConfiguration.java new file mode 100644 index 0000000..54f3951 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/LoggingAspectConfiguration.java @@ -0,0 +1,17 @@ +package com.nayakfamily.familytree.config; + +import com.nayakfamily.familytree.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/nayakfamily/familytree/config/LoggingConfiguration.java b/src/main/java/com/nayakfamily/familytree/config/LoggingConfiguration.java new file mode 100644 index 0000000..d916904 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/LoggingConfiguration.java @@ -0,0 +1,47 @@ +package com.nayakfamily.familytree.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/nayakfamily/familytree/config/SecurityConfiguration.java b/src/main/java/com/nayakfamily/familytree/config/SecurityConfiguration.java new file mode 100644 index 0000000..bc5a422 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/SecurityConfiguration.java @@ -0,0 +1,111 @@ +package com.nayakfamily.familytree.config; + +import static org.springframework.security.config.Customizer.withDefaults; +import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; + +import com.nayakfamily.familytree.security.*; +import com.nayakfamily.familytree.web.filter.SpaWebFilter; +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.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig; +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.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint; +import org.springframework.security.oauth2.server.resource.web.access.BearerTokenAccessDeniedHandler; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.config.JHipsterProperties; + +@Configuration +@EnableMethodSecurity(securedEnabled = true) +public class SecurityConfiguration { + + private final Environment env; + + private final JHipsterProperties jHipsterProperties; + + public SecurityConfiguration(Environment env, JHipsterProperties jHipsterProperties) { + this.env = env; + this.jHipsterProperties = jHipsterProperties; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception { + http + .cors(withDefaults()) + .csrf(csrf -> csrf.disable()) + .addFilterAfter(new SpaWebFilter(), BasicAuthenticationFilter.class) + .headers( + headers -> + headers + .contentSecurityPolicy(csp -> csp.policyDirectives(jHipsterProperties.getSecurity().getContentSecurityPolicy())) + .frameOptions(FrameOptionsConfig::sameOrigin) + .referrerPolicy( + referrer -> referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN) + ) + .permissionsPolicy( + permissions -> + permissions.policy( + "camera=(), fullscreen=(self), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=()" + ) + ) + ) + .authorizeHttpRequests( + authz -> + // prettier-ignore + authz + .requestMatchers(mvc.pattern("/index.html"), mvc.pattern("/*.js"), mvc.pattern("/*.txt"), mvc.pattern("/*.json"), mvc.pattern("/*.map"), mvc.pattern("/*.css")).permitAll() + .requestMatchers(mvc.pattern("/*.ico"), mvc.pattern("/*.png"), mvc.pattern("/*.svg"), mvc.pattern("/*.webapp")).permitAll() + .requestMatchers(mvc.pattern("/app/**")).permitAll() + .requestMatchers(mvc.pattern("/i18n/**")).permitAll() + .requestMatchers(mvc.pattern("/content/**")).permitAll() + .requestMatchers(mvc.pattern("/swagger-ui/**")).permitAll() + .requestMatchers(mvc.pattern(HttpMethod.POST, "/api/authenticate")).permitAll() + .requestMatchers(mvc.pattern(HttpMethod.GET, "/api/authenticate")).permitAll() + .requestMatchers(mvc.pattern("/api/register")).permitAll() + .requestMatchers(mvc.pattern("/api/activate")).permitAll() + .requestMatchers(mvc.pattern("/api/account/reset-password/init")).permitAll() + .requestMatchers(mvc.pattern("/api/account/reset-password/finish")).permitAll() + .requestMatchers(mvc.pattern("/api/admin/**")).hasAuthority(AuthoritiesConstants.ADMIN) + .requestMatchers(mvc.pattern("/api/**")).authenticated() + .requestMatchers(mvc.pattern("/v3/api-docs/**")).hasAuthority(AuthoritiesConstants.ADMIN) + .requestMatchers(mvc.pattern("/management/health")).permitAll() + .requestMatchers(mvc.pattern("/management/health/**")).permitAll() + .requestMatchers(mvc.pattern("/management/info")).permitAll() + .requestMatchers(mvc.pattern("/management/prometheus")).permitAll() + .requestMatchers(mvc.pattern("/management/**")).hasAuthority(AuthoritiesConstants.ADMIN) + ) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling( + exceptions -> + exceptions + .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint()) + .accessDeniedHandler(new BearerTokenAccessDeniedHandler()) + ) + .oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults())); + if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) { + http.authorizeHttpRequests(authz -> authz.requestMatchers(antMatcher("/h2-console/**")).permitAll()); + } + return http.build(); + } + + @Bean + MvcRequestMatcher.Builder mvc(HandlerMappingIntrospector introspector) { + return new MvcRequestMatcher.Builder(introspector); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/config/SecurityJwtConfiguration.java b/src/main/java/com/nayakfamily/familytree/config/SecurityJwtConfiguration.java new file mode 100644 index 0000000..3a1fb8a --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/SecurityJwtConfiguration.java @@ -0,0 +1,76 @@ +package com.nayakfamily.familytree.config; + +import static com.nayakfamily.familytree.security.SecurityUtils.AUTHORITIES_KEY; +import static com.nayakfamily.familytree.security.SecurityUtils.JWT_ALGORITHM; + +import com.nayakfamily.familytree.management.SecurityMetersService; +import com.nimbusds.jose.jwk.source.ImmutableSecret; +import com.nimbusds.jose.util.Base64; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; + +@Configuration +public class SecurityJwtConfiguration { + + private final Logger log = LoggerFactory.getLogger(SecurityJwtConfiguration.class); + + @Value("${jhipster.security.authentication.jwt.base64-secret}") + private String jwtKey; + + @Bean + public JwtDecoder jwtDecoder(SecurityMetersService metersService) { + NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(getSecretKey()).macAlgorithm(JWT_ALGORITHM).build(); + return token -> { + try { + return jwtDecoder.decode(token); + } catch (Exception e) { + if (e.getMessage().contains("Invalid signature")) { + metersService.trackTokenInvalidSignature(); + } else if (e.getMessage().contains("Jwt expired at")) { + metersService.trackTokenExpired(); + } else if ( + e.getMessage().contains("Invalid JWT serialization") || + e.getMessage().contains("Malformed token") || + e.getMessage().contains("Invalid unsecured/JWS/JWE") + ) { + metersService.trackTokenMalformed(); + } else { + log.error("Unknown JWT error {}", e.getMessage()); + } + throw e; + } + }; + } + + @Bean + public JwtEncoder jwtEncoder() { + return new NimbusJwtEncoder(new ImmutableSecret<>(getSecretKey())); + } + + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); + grantedAuthoritiesConverter.setAuthorityPrefix(""); + grantedAuthoritiesConverter.setAuthoritiesClaimName(AUTHORITIES_KEY); + + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); + return jwtAuthenticationConverter; + } + + private SecretKey getSecretKey() { + byte[] keyBytes = Base64.from(jwtKey).decode(); + return new SecretKeySpec(keyBytes, 0, keyBytes.length, JWT_ALGORITHM.getName()); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/config/StaticResourcesWebConfiguration.java b/src/main/java/com/nayakfamily/familytree/config/StaticResourcesWebConfiguration.java new file mode 100644 index 0000000..50652f4 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/StaticResourcesWebConfiguration.java @@ -0,0 +1,47 @@ +package com.nayakfamily.familytree.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 = { "classpath:/static/", "classpath:/static/content/", "classpath:/static/i18n/" }; + protected static final String[] RESOURCE_PATHS = { "/*.js", "/*.css", "/*.svg", "/*.png", "*.ico", "/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/nayakfamily/familytree/config/WebConfigurer.java b/src/main/java/com/nayakfamily/familytree/config/WebConfigurer.java new file mode 100644 index 0000000..e4bb660 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/WebConfigurer.java @@ -0,0 +1,110 @@ +package com.nayakfamily.familytree.config; + +import static java.net.URLDecoder.decode; + +import jakarta.servlet.*; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +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) { + 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 ./mvnw spring-boot:run, set location of the static web assets. + setLocationForStaticAssets(server); + } + + private void setLocationForStaticAssets(WebServerFactory server) { + if (server instanceof ConfigurableServletWebServerFactory servletWebServer) { + File root; + String prefixPath = resolvePathPrefix(); + root = new File(prefixPath + "target/classes/static/"); + if (root.exists() && root.isDirectory()) { + servletWebServer.setDocumentRoot(root); + } + } + } + + /** + * Resolve path prefix to static resources. + */ + private String resolvePathPrefix() { + String fullExecutablePath = decode(this.getClass().getResource("").getPath(), StandardCharsets.UTF_8); + String rootPath = Paths.get(".").toUri().normalize().getPath(); + String extractedPath = fullExecutablePath.replace(rootPath, ""); + int extractionEndIndex = extractedPath.indexOf("target/"); + 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()) || !CollectionUtils.isEmpty(config.getAllowedOriginPatterns())) { + log.debug("Registering CORS filter"); + source.registerCorsConfiguration("/api/**", config); + source.registerCorsConfiguration("/management/**", config); + source.registerCorsConfiguration("/v3/api-docs", 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/nayakfamily/familytree/config/package-info.java b/src/main/java/com/nayakfamily/familytree/config/package-info.java new file mode 100644 index 0000000..08874e4 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/config/package-info.java @@ -0,0 +1,4 @@ +/** + * Application configuration. + */ +package com.nayakfamily.familytree.config; diff --git a/src/main/java/com/nayakfamily/familytree/domain/AbstractAuditingEntity.java b/src/main/java/com/nayakfamily/familytree/domain/AbstractAuditingEntity.java new file mode 100644 index 0000000..47eff3a --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/domain/AbstractAuditingEntity.java @@ -0,0 +1,75 @@ +package com.nayakfamily.familytree.domain; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import java.io.Serializable; +import java.time.Instant; +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) +@JsonIgnoreProperties(value = { "createdBy", "createdDate", "lastModifiedBy", "lastModifiedDate" }, allowGetters = true) +public abstract class AbstractAuditingEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + public abstract T getId(); + + @CreatedBy + @Column(name = "created_by", nullable = false, length = 50, updatable = false) + private String createdBy; + + @CreatedDate + @Column(name = "created_date", updatable = false) + private Instant createdDate = Instant.now(); + + @LastModifiedBy + @Column(name = "last_modified_by", length = 50) + private String lastModifiedBy; + + @LastModifiedDate + @Column(name = "last_modified_date") + 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/nayakfamily/familytree/domain/Authority.java b/src/main/java/com/nayakfamily/familytree/domain/Authority.java new file mode 100644 index 0000000..767a843 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/domain/Authority.java @@ -0,0 +1,95 @@ +package com.nayakfamily.familytree.domain; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.io.Serializable; +import java.util.Objects; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.springframework.data.domain.Persistable; + +/** + * A Authority. + */ +@Entity +@Table(name = "jhi_authority") +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE) +@JsonIgnoreProperties(value = { "new", "id" }) +@SuppressWarnings("common-java:DuplicatedBlocks") +public class Authority implements Serializable, Persistable { + + private static final long serialVersionUID = 1L; + + @NotNull + @Size(max = 50) + @Id + @Column(name = "name", length = 50, nullable = false) + private String name; + + @Transient + private boolean isPersisted; + + // jhipster-needle-entity-add-field - JHipster will add fields here + + public String getName() { + return this.name; + } + + public Authority name(String name) { + this.setName(name); + return this; + } + + public void setName(String name) { + this.name = name; + } + + @PostLoad + @PostPersist + public void updateEntityState() { + this.setIsPersisted(); + } + + @Override + public String getId() { + return this.name; + } + + @Transient + @Override + public boolean isNew() { + return !this.isPersisted; + } + + public Authority setIsPersisted() { + this.isPersisted = true; + return this; + } + + // 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 Authority)) { + return false; + } + return getName() != null && getName().equals(((Authority) o).getName()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getName()); + } + + // prettier-ignore + @Override + public String toString() { + return "Authority{" + + "name=" + getName() + + "}"; + } +} diff --git a/src/main/java/com/nayakfamily/familytree/domain/User.java b/src/main/java/com/nayakfamily/familytree/domain/User.java new file mode 100644 index 0000000..6bd6598 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/domain/User.java @@ -0,0 +1,232 @@ +package com.nayakfamily.familytree.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.nayakfamily.familytree.config.Constants; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import java.io.Serializable; +import java.time.Instant; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; +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.SEQUENCE, generator = "sequenceGenerator") + @SequenceGenerator(name = "sequenceGenerator") + 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/nayakfamily/familytree/domain/package-info.java b/src/main/java/com/nayakfamily/familytree/domain/package-info.java new file mode 100644 index 0000000..e061014 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/domain/package-info.java @@ -0,0 +1,4 @@ +/** + * Domain objects. + */ +package com.nayakfamily.familytree.domain; diff --git a/src/main/java/com/nayakfamily/familytree/management/SecurityMetersService.java b/src/main/java/com/nayakfamily/familytree/management/SecurityMetersService.java new file mode 100644 index 0000000..2a47c2f --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/management/SecurityMetersService.java @@ -0,0 +1,50 @@ +package com.nayakfamily.familytree.management; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.stereotype.Service; + +@Service +public class SecurityMetersService { + + public static final String INVALID_TOKENS_METER_NAME = "security.authentication.invalid-tokens"; + public static final String INVALID_TOKENS_METER_DESCRIPTION = + "Indicates validation error count of the tokens presented by the clients."; + public static final String INVALID_TOKENS_METER_BASE_UNIT = "errors"; + public static final String INVALID_TOKENS_METER_CAUSE_DIMENSION = "cause"; + + private final Counter tokenInvalidSignatureCounter; + private final Counter tokenExpiredCounter; + private final Counter tokenUnsupportedCounter; + private final Counter tokenMalformedCounter; + + public SecurityMetersService(MeterRegistry registry) { + this.tokenInvalidSignatureCounter = invalidTokensCounterForCauseBuilder("invalid-signature").register(registry); + this.tokenExpiredCounter = invalidTokensCounterForCauseBuilder("expired").register(registry); + this.tokenUnsupportedCounter = invalidTokensCounterForCauseBuilder("unsupported").register(registry); + this.tokenMalformedCounter = invalidTokensCounterForCauseBuilder("malformed").register(registry); + } + + private Counter.Builder invalidTokensCounterForCauseBuilder(String cause) { + return Counter.builder(INVALID_TOKENS_METER_NAME) + .baseUnit(INVALID_TOKENS_METER_BASE_UNIT) + .description(INVALID_TOKENS_METER_DESCRIPTION) + .tag(INVALID_TOKENS_METER_CAUSE_DIMENSION, cause); + } + + public void trackTokenInvalidSignature() { + this.tokenInvalidSignatureCounter.increment(); + } + + public void trackTokenExpired() { + this.tokenExpiredCounter.increment(); + } + + public void trackTokenUnsupported() { + this.tokenUnsupportedCounter.increment(); + } + + public void trackTokenMalformed() { + this.tokenMalformedCounter.increment(); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/management/package-info.java b/src/main/java/com/nayakfamily/familytree/management/package-info.java new file mode 100644 index 0000000..7dfae5a --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/management/package-info.java @@ -0,0 +1,4 @@ +/** + * Application management. + */ +package com.nayakfamily.familytree.management; diff --git a/src/main/java/com/nayakfamily/familytree/package-info.java b/src/main/java/com/nayakfamily/familytree/package-info.java new file mode 100644 index 0000000..e93545a --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/package-info.java @@ -0,0 +1,4 @@ +/** + * Application root. + */ +package com.nayakfamily.familytree; diff --git a/src/main/java/com/nayakfamily/familytree/repository/AuthorityRepository.java b/src/main/java/com/nayakfamily/familytree/repository/AuthorityRepository.java new file mode 100644 index 0000000..8baf845 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/repository/AuthorityRepository.java @@ -0,0 +1,12 @@ +package com.nayakfamily.familytree.repository; + +import com.nayakfamily.familytree.domain.Authority; +import org.springframework.data.jpa.repository.*; +import org.springframework.stereotype.Repository; + +/** + * Spring Data JPA repository for the Authority entity. + */ +@SuppressWarnings("unused") +@Repository +public interface AuthorityRepository extends JpaRepository {} diff --git a/src/main/java/com/nayakfamily/familytree/repository/UserRepository.java b/src/main/java/com/nayakfamily/familytree/repository/UserRepository.java new file mode 100644 index 0000000..42e05dc --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/repository/UserRepository.java @@ -0,0 +1,36 @@ +package com.nayakfamily.familytree.repository; + +import com.nayakfamily.familytree.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.*; +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/nayakfamily/familytree/repository/package-info.java b/src/main/java/com/nayakfamily/familytree/repository/package-info.java new file mode 100644 index 0000000..c8e50d3 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/repository/package-info.java @@ -0,0 +1,4 @@ +/** + * Repository layer. + */ +package com.nayakfamily.familytree.repository; diff --git a/src/main/java/com/nayakfamily/familytree/security/AuthoritiesConstants.java b/src/main/java/com/nayakfamily/familytree/security/AuthoritiesConstants.java new file mode 100644 index 0000000..a6200e7 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/security/AuthoritiesConstants.java @@ -0,0 +1,15 @@ +package com.nayakfamily.familytree.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/nayakfamily/familytree/security/DomainUserDetailsService.java b/src/main/java/com/nayakfamily/familytree/security/DomainUserDetailsService.java new file mode 100644 index 0000000..bda2078 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/security/DomainUserDetailsService.java @@ -0,0 +1,62 @@ +package com.nayakfamily.familytree.security; + +import com.nayakfamily.familytree.domain.Authority; +import com.nayakfamily.familytree.domain.User; +import com.nayakfamily.familytree.repository.UserRepository; +import java.util.*; +import org.hibernate.validator.internal.constraintvalidators.hv.EmailValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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(readOnly = true) + 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::getName) + .map(SimpleGrantedAuthority::new) + .toList(); + return new org.springframework.security.core.userdetails.User(user.getLogin(), user.getPassword(), grantedAuthorities); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/security/SecurityUtils.java b/src/main/java/com/nayakfamily/familytree/security/SecurityUtils.java new file mode 100644 index 0000000..0911b2f --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/security/SecurityUtils.java @@ -0,0 +1,106 @@ +package com.nayakfamily.familytree.security; + +import java.util.Arrays; +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; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jwt.Jwt; + +/** + * Utility class for Spring Security. + */ +public final class SecurityUtils { + + public static final MacAlgorithm JWT_ALGORITHM = MacAlgorithm.HS512; + + public static final String AUTHORITIES_KEY = "auth"; + + 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 springSecurityUser) { + return springSecurityUser.getUsername(); + } else if (authentication.getPrincipal() instanceof Jwt jwt) { + return jwt.getSubject(); + } else if (authentication.getPrincipal() instanceof String s) { + return s; + } + 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 any of the authorities. + * + * @param authorities the authorities to check. + * @return true if the current user has any of the authorities, false otherwise. + */ + public static boolean hasCurrentUserAnyOfAuthorities(String... authorities) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return ( + authentication != null && getAuthorities(authentication).anyMatch(authority -> Arrays.asList(authorities).contains(authority)) + ); + } + + /** + * Checks if the current user has none of the authorities. + * + * @param authorities the authorities to check. + * @return true if the current user has none of the authorities, false otherwise. + */ + public static boolean hasCurrentUserNoneOfAuthorities(String... authorities) { + return !hasCurrentUserAnyOfAuthorities(authorities); + } + + /** + * 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) { + return hasCurrentUserAnyOfAuthorities(authority); + } + + private static Stream getAuthorities(Authentication authentication) { + return authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/security/SpringSecurityAuditorAware.java b/src/main/java/com/nayakfamily/familytree/security/SpringSecurityAuditorAware.java new file mode 100644 index 0000000..cf06bfb --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/security/SpringSecurityAuditorAware.java @@ -0,0 +1,18 @@ +package com.nayakfamily.familytree.security; + +import com.nayakfamily.familytree.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/nayakfamily/familytree/security/UserNotActivatedException.java b/src/main/java/com/nayakfamily/familytree/security/UserNotActivatedException.java new file mode 100644 index 0000000..dfdc2df --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/security/UserNotActivatedException.java @@ -0,0 +1,19 @@ +package com.nayakfamily.familytree.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/nayakfamily/familytree/security/package-info.java b/src/main/java/com/nayakfamily/familytree/security/package-info.java new file mode 100644 index 0000000..a9403f2 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/security/package-info.java @@ -0,0 +1,4 @@ +/** + * Application security utilities. + */ +package com.nayakfamily.familytree.security; diff --git a/src/main/java/com/nayakfamily/familytree/service/EmailAlreadyUsedException.java b/src/main/java/com/nayakfamily/familytree/service/EmailAlreadyUsedException.java new file mode 100644 index 0000000..efe3eea --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/service/EmailAlreadyUsedException.java @@ -0,0 +1,10 @@ +package com.nayakfamily.familytree.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/nayakfamily/familytree/service/InvalidPasswordException.java b/src/main/java/com/nayakfamily/familytree/service/InvalidPasswordException.java new file mode 100644 index 0000000..be32268 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/service/InvalidPasswordException.java @@ -0,0 +1,10 @@ +package com.nayakfamily.familytree.service; + +public class InvalidPasswordException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public InvalidPasswordException() { + super("Incorrect password"); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/service/MailService.java b/src/main/java/com/nayakfamily/familytree/service/MailService.java new file mode 100644 index 0000000..7f625b9 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/service/MailService.java @@ -0,0 +1,120 @@ +package com.nayakfamily.familytree.service; + +import com.nayakfamily.familytree.domain.User; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +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.spring6.SpringTemplateEngine; +import tech.jhipster.config.JHipsterProperties; + +/** + * Service for sending emails asynchronously. + *

+ * 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) { + this.sendEmailSync(to, subject, content, isMultipart, isHtml); + } + + private void sendEmailSync(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) { + this.sendEmailFromTemplateSync(user, templateName, titleKey); + } + + private void sendEmailFromTemplateSync(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); + this.sendEmailSync(user.getEmail(), subject, content, false, true); + } + + @Async + public void sendActivationEmail(User user) { + log.debug("Sending activation email to '{}'", user.getEmail()); + this.sendEmailFromTemplateSync(user, "mail/activationEmail", "email.activation.title"); + } + + @Async + public void sendCreationEmail(User user) { + log.debug("Sending creation email to '{}'", user.getEmail()); + this.sendEmailFromTemplateSync(user, "mail/creationEmail", "email.activation.title"); + } + + @Async + public void sendPasswordResetMail(User user) { + log.debug("Sending password reset email to '{}'", user.getEmail()); + this.sendEmailFromTemplateSync(user, "mail/passwordResetEmail", "email.reset.title"); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/service/UserService.java b/src/main/java/com/nayakfamily/familytree/service/UserService.java new file mode 100644 index 0000000..135cef8 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/service/UserService.java @@ -0,0 +1,324 @@ +package com.nayakfamily.familytree.service; + +import com.nayakfamily.familytree.config.Constants; +import com.nayakfamily.familytree.domain.Authority; +import com.nayakfamily.familytree.domain.User; +import com.nayakfamily.familytree.repository.AuthorityRepository; +import com.nayakfamily.familytree.repository.UserRepository; +import com.nayakfamily.familytree.security.AuthoritiesConstants; +import com.nayakfamily.familytree.security.SecurityUtils; +import com.nayakfamily.familytree.service.dto.AdminUserDTO; +import com.nayakfamily.familytree.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().minus(1, ChronoUnit.DAYS))) + .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); + userRepository.save(user); + 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); + userRepository.save(user); + 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).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/nayakfamily/familytree/service/UsernameAlreadyUsedException.java b/src/main/java/com/nayakfamily/familytree/service/UsernameAlreadyUsedException.java new file mode 100644 index 0000000..841acdf --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/service/UsernameAlreadyUsedException.java @@ -0,0 +1,10 @@ +package com.nayakfamily.familytree.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/nayakfamily/familytree/service/dto/AdminUserDTO.java b/src/main/java/com/nayakfamily/familytree/service/dto/AdminUserDTO.java new file mode 100644 index 0000000..87072b9 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/service/dto/AdminUserDTO.java @@ -0,0 +1,196 @@ +package com.nayakfamily.familytree.service.dto; + +import com.nayakfamily.familytree.config.Constants; +import com.nayakfamily.familytree.domain.Authority; +import com.nayakfamily.familytree.domain.User; +import jakarta.validation.constraints.*; +import java.io.Serializable; +import java.time.Instant; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A DTO representing a user, with his authorities. + */ +public class AdminUserDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + 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/nayakfamily/familytree/service/dto/PasswordChangeDTO.java b/src/main/java/com/nayakfamily/familytree/service/dto/PasswordChangeDTO.java new file mode 100644 index 0000000..ec4f952 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/service/dto/PasswordChangeDTO.java @@ -0,0 +1,39 @@ +package com.nayakfamily.familytree.service.dto; + +import java.io.Serializable; + +/** + * A DTO representing a password change required data - current and new password. + */ +public class PasswordChangeDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + 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/nayakfamily/familytree/service/dto/UserDTO.java b/src/main/java/com/nayakfamily/familytree/service/dto/UserDTO.java new file mode 100644 index 0000000..b6e327c --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/service/dto/UserDTO.java @@ -0,0 +1,51 @@ +package com.nayakfamily.familytree.service.dto; + +import com.nayakfamily.familytree.domain.User; +import java.io.Serializable; + +/** + * A DTO representing a user, with only the public attributes. + */ +public class UserDTO implements Serializable { + + private static final long serialVersionUID = 1L; + + 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/nayakfamily/familytree/service/dto/package-info.java b/src/main/java/com/nayakfamily/familytree/service/dto/package-info.java new file mode 100644 index 0000000..b912851 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/service/dto/package-info.java @@ -0,0 +1,4 @@ +/** + * Data transfer objects for rest mapping. + */ +package com.nayakfamily.familytree.service.dto; diff --git a/src/main/java/com/nayakfamily/familytree/service/mapper/UserMapper.java b/src/main/java/com/nayakfamily/familytree/service/mapper/UserMapper.java new file mode 100644 index 0000000..17024d8 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/service/mapper/UserMapper.java @@ -0,0 +1,146 @@ +package com.nayakfamily.familytree.service.mapper; + +import com.nayakfamily.familytree.domain.Authority; +import com.nayakfamily.familytree.domain.User; +import com.nayakfamily.familytree.service.dto.AdminUserDTO; +import com.nayakfamily.familytree.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).toList(); + } + + public UserDTO userToUserDTO(User user) { + return new UserDTO(user); + } + + public List usersToAdminUserDTOs(List users) { + return users.stream().filter(Objects::nonNull).map(this::userToAdminUserDTO).toList(); + } + + public AdminUserDTO userToAdminUserDTO(User user) { + return new AdminUserDTO(user); + } + + public List userDTOsToUsers(List userDTOs) { + return userDTOs.stream().filter(Objects::nonNull).map(this::userDTOToUser).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 Collections.emptySet(); + } + + 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 Collections.emptySet(); + } + + Set userSet = new HashSet<>(); + for (User userEntity : users) { + userSet.add(this.toDtoLogin(userEntity)); + } + + return userSet; + } +} diff --git a/src/main/java/com/nayakfamily/familytree/service/mapper/package-info.java b/src/main/java/com/nayakfamily/familytree/service/mapper/package-info.java new file mode 100644 index 0000000..44390b5 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/service/mapper/package-info.java @@ -0,0 +1,4 @@ +/** + * Data transfer objects mappers. + */ +package com.nayakfamily.familytree.service.mapper; diff --git a/src/main/java/com/nayakfamily/familytree/service/package-info.java b/src/main/java/com/nayakfamily/familytree/service/package-info.java new file mode 100644 index 0000000..fb5fc6b --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/service/package-info.java @@ -0,0 +1,4 @@ +/** + * Service layer. + */ +package com.nayakfamily.familytree.service; diff --git a/src/main/java/com/nayakfamily/familytree/web/filter/SpaWebFilter.java b/src/main/java/com/nayakfamily/familytree/web/filter/SpaWebFilter.java new file mode 100644 index 0000000..6a2b59c --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/filter/SpaWebFilter.java @@ -0,0 +1,34 @@ +package com.nayakfamily.familytree.web.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.web.filter.OncePerRequestFilter; + +public class SpaWebFilter extends OncePerRequestFilter { + + /** + * Forwards any unmapped paths (except those containing a period) to the client {@code index.html}. + */ + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // Request URI includes the contextPath if any, removed it. + String path = request.getRequestURI().substring(request.getContextPath().length()); + if ( + !path.startsWith("/api") && + !path.startsWith("/management") && + !path.startsWith("/v3/api-docs") && + !path.startsWith("/h2-console") && + !path.contains(".") && + path.matches("/(.*)") + ) { + request.getRequestDispatcher("/index.html").forward(request, response); + return; + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/web/filter/package-info.java b/src/main/java/com/nayakfamily/familytree/web/filter/package-info.java new file mode 100644 index 0000000..fdf11c5 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/filter/package-info.java @@ -0,0 +1,4 @@ +/** + * Request chain filters. + */ +package com.nayakfamily.familytree.web.filter; diff --git a/src/main/java/com/nayakfamily/familytree/web/rest/AccountResource.java b/src/main/java/com/nayakfamily/familytree/web/rest/AccountResource.java new file mode 100644 index 0000000..2e8b029 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/AccountResource.java @@ -0,0 +1,180 @@ +package com.nayakfamily.familytree.web.rest; + +import com.nayakfamily.familytree.domain.User; +import com.nayakfamily.familytree.repository.UserRepository; +import com.nayakfamily.familytree.security.SecurityUtils; +import com.nayakfamily.familytree.service.MailService; +import com.nayakfamily.familytree.service.UserService; +import com.nayakfamily.familytree.service.dto.AdminUserDTO; +import com.nayakfamily.familytree.service.dto.PasswordChangeDTO; +import com.nayakfamily.familytree.web.rest.errors.*; +import com.nayakfamily.familytree.web.rest.vm.KeyAndPasswordVM; +import com.nayakfamily.familytree.web.rest.vm.ManagedUserVM; +import jakarta.validation.Valid; +import java.util.*; +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 /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.orElseThrow().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.orElseThrow()); + } 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/nayakfamily/familytree/web/rest/AuthenticateController.java b/src/main/java/com/nayakfamily/familytree/web/rest/AuthenticateController.java new file mode 100644 index 0000000..f49281b --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/AuthenticateController.java @@ -0,0 +1,124 @@ +package com.nayakfamily.familytree.web.rest; + +import static com.nayakfamily.familytree.security.SecurityUtils.AUTHORITIES_KEY; +import static com.nayakfamily.familytree.security.SecurityUtils.JWT_ALGORITHM; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.nayakfamily.familytree.web.rest.vm.LoginVM; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +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.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.JwsHeader; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.JwtEncoderParameters; +import org.springframework.web.bind.annotation.*; + +/** + * Controller to authenticate users. + */ +@RestController +@RequestMapping("/api") +public class AuthenticateController { + + private final Logger log = LoggerFactory.getLogger(AuthenticateController.class); + + private final JwtEncoder jwtEncoder; + + @Value("${jhipster.security.authentication.jwt.token-validity-in-seconds:0}") + private long tokenValidityInSeconds; + + @Value("${jhipster.security.authentication.jwt.token-validity-in-seconds-for-remember-me:0}") + private long tokenValidityInSecondsForRememberMe; + + private final AuthenticationManagerBuilder authenticationManagerBuilder; + + public AuthenticateController(JwtEncoder jwtEncoder, AuthenticationManagerBuilder authenticationManagerBuilder) { + this.jwtEncoder = jwtEncoder; + 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 = this.createToken(authentication, loginVM.isRememberMe()); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.setBearerAuth(jwt); + return new ResponseEntity<>(new JWTToken(jwt), httpHeaders, HttpStatus.OK); + } + + /** + * {@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(); + } + + public String createToken(Authentication authentication, boolean rememberMe) { + String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(" ")); + + Instant now = Instant.now(); + Instant validity; + if (rememberMe) { + validity = now.plus(this.tokenValidityInSecondsForRememberMe, ChronoUnit.SECONDS); + } else { + validity = now.plus(this.tokenValidityInSeconds, ChronoUnit.SECONDS); + } + + // @formatter:off + JwtClaimsSet claims = JwtClaimsSet.builder() + .issuedAt(now) + .expiresAt(validity) + .subject(authentication.getName()) + .claim(AUTHORITIES_KEY, authorities) + .build(); + + JwsHeader jwsHeader = JwsHeader.with(JWT_ALGORITHM).build(); + return this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims)).getTokenValue(); + } + + /** + * 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/nayakfamily/familytree/web/rest/AuthorityResource.java b/src/main/java/com/nayakfamily/familytree/web/rest/AuthorityResource.java new file mode 100644 index 0000000..45e8092 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/AuthorityResource.java @@ -0,0 +1,101 @@ +package com.nayakfamily.familytree.web.rest; + +import com.nayakfamily.familytree.domain.Authority; +import com.nayakfamily.familytree.repository.AuthorityRepository; +import com.nayakfamily.familytree.web.rest.errors.BadRequestAlertException; +import jakarta.validation.Valid; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.*; +import tech.jhipster.web.util.HeaderUtil; +import tech.jhipster.web.util.ResponseUtil; + +/** + * REST controller for managing {@link com.nayakfamily.familytree.domain.Authority}. + */ +@RestController +@RequestMapping("/api/authorities") +@Transactional +public class AuthorityResource { + + private final Logger log = LoggerFactory.getLogger(AuthorityResource.class); + + private static final String ENTITY_NAME = "adminAuthority"; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final AuthorityRepository authorityRepository; + + public AuthorityResource(AuthorityRepository authorityRepository) { + this.authorityRepository = authorityRepository; + } + + /** + * {@code POST /authorities} : Create a new authority. + * + * @param authority the authority to create. + * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new authority, or with status {@code 400 (Bad Request)} if the authority has already an ID. + * @throws URISyntaxException if the Location URI syntax is incorrect. + */ + @PostMapping("") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + public ResponseEntity createAuthority(@Valid @RequestBody Authority authority) throws URISyntaxException { + log.debug("REST request to save Authority : {}", authority); + if (authorityRepository.existsById(authority.getName())) { + throw new BadRequestAlertException("authority already exists", ENTITY_NAME, "idexists"); + } + authority = authorityRepository.save(authority); + return ResponseEntity.created(new URI("/api/authorities/" + authority.getName())) + .headers(HeaderUtil.createEntityCreationAlert(applicationName, false, ENTITY_NAME, authority.getName())) + .body(authority); + } + + /** + * {@code GET /authorities} : get all the authorities. + * + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of authorities in body. + */ + @GetMapping("") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + public List getAllAuthorities() { + log.debug("REST request to get all Authorities"); + return authorityRepository.findAll(); + } + + /** + * {@code GET /authorities/:id} : get the "id" authority. + * + * @param id the id of the authority to retrieve. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the authority, or with status {@code 404 (Not Found)}. + */ + @GetMapping("/{id}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + public ResponseEntity getAuthority(@PathVariable("id") String id) { + log.debug("REST request to get Authority : {}", id); + Optional authority = authorityRepository.findById(id); + return ResponseUtil.wrapOrNotFound(authority); + } + + /** + * {@code DELETE /authorities/:id} : delete the "id" authority. + * + * @param id the id of the authority to delete. + * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}. + */ + @DeleteMapping("/{id}") + @PreAuthorize("hasAnyAuthority('ROLE_ADMIN')") + public ResponseEntity deleteAuthority(@PathVariable("id") String id) { + log.debug("REST request to delete Authority : {}", id); + authorityRepository.deleteById(id); + return ResponseEntity.noContent().headers(HeaderUtil.createEntityDeletionAlert(applicationName, false, ENTITY_NAME, id)).build(); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/web/rest/PublicUserResource.java b/src/main/java/com/nayakfamily/familytree/web/rest/PublicUserResource.java new file mode 100644 index 0000000..25cf734 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/PublicUserResource.java @@ -0,0 +1,56 @@ +package com.nayakfamily.familytree.web.rest; + +import com.nayakfamily.familytree.service.UserService; +import com.nayakfamily.familytree.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 public information - calling this method is 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(@org.springdoc.core.annotations.ParameterObject 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); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/web/rest/UserResource.java b/src/main/java/com/nayakfamily/familytree/web/rest/UserResource.java new file mode 100644 index 0000000..39e58db --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/UserResource.java @@ -0,0 +1,213 @@ +package com.nayakfamily.familytree.web.rest; + +import com.nayakfamily.familytree.config.Constants; +import com.nayakfamily.familytree.domain.User; +import com.nayakfamily.familytree.repository.UserRepository; +import com.nayakfamily.familytree.security.AuthoritiesConstants; +import com.nayakfamily.familytree.service.MailService; +import com.nayakfamily.familytree.service.UserService; +import com.nayakfamily.familytree.service.dto.AdminUserDTO; +import com.nayakfamily.familytree.web.rest.errors.BadRequestAlertException; +import com.nayakfamily.familytree.web.rest.errors.EmailAlreadyUsedException; +import com.nayakfamily.familytree.web.rest.errors.LoginAlreadyUsedException; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Pattern; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; +import java.util.Collections; +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 com.nayakfamily.familytree.domain.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", + "createdBy", + "createdDate", + "lastModifiedBy", + "lastModifiedDate" + ) + ); + + 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", "/users/{login}" }) + @PreAuthorize("hasAuthority(\"" + AuthoritiesConstants.ADMIN + "\")") + public ResponseEntity updateUser( + @PathVariable(name = "login", required = false) @Pattern(regexp = Constants.LOGIN_REGEX) String login, + @Valid @RequestBody AdminUserDTO userDTO + ) { + log.debug("REST request to update User : {}", userDTO); + Optional existingUser = userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()); + if (existingUser.isPresent() && (!existingUser.orElseThrow().getId().equals(userDTO.getId()))) { + throw new EmailAlreadyUsedException(); + } + existingUser = userRepository.findOneByLogin(userDTO.getLogin().toLowerCase()); + if (existingUser.isPresent() && (!existingUser.orElseThrow().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(@org.springdoc.core.annotations.ParameterObject 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("login") @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("login") @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/nayakfamily/familytree/web/rest/errors/BadRequestAlertException.java b/src/main/java/com/nayakfamily/familytree/web/rest/errors/BadRequestAlertException.java new file mode 100644 index 0000000..6931c5c --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/errors/BadRequestAlertException.java @@ -0,0 +1,49 @@ +package com.nayakfamily.familytree.web.rest.errors; + +import java.net.URI; +import org.springframework.http.HttpStatus; +import org.springframework.web.ErrorResponseException; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder; + +@SuppressWarnings("java:S110") // Inheritance tree of classes should not be too deep +public class BadRequestAlertException extends ErrorResponseException { + + 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( + HttpStatus.BAD_REQUEST, + ProblemDetailWithCauseBuilder.instance() + .withStatus(HttpStatus.BAD_REQUEST.value()) + .withType(type) + .withTitle(defaultMessage) + .withProperty("message", "error." + errorKey) + .withProperty("params", entityName) + .build(), + null + ); + this.entityName = entityName; + this.errorKey = errorKey; + } + + public String getEntityName() { + return entityName; + } + + public String getErrorKey() { + return errorKey; + } + + public ProblemDetailWithCause getProblemDetailWithCause() { + return (ProblemDetailWithCause) this.getBody(); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/web/rest/errors/EmailAlreadyUsedException.java b/src/main/java/com/nayakfamily/familytree/web/rest/errors/EmailAlreadyUsedException.java new file mode 100644 index 0000000..d0d108f --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/errors/EmailAlreadyUsedException.java @@ -0,0 +1,11 @@ +package com.nayakfamily.familytree.web.rest.errors; + +@SuppressWarnings("java:S110") // Inheritance tree of classes should not be too deep +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/nayakfamily/familytree/web/rest/errors/ErrorConstants.java b/src/main/java/com/nayakfamily/familytree/web/rest/errors/ErrorConstants.java new file mode 100644 index 0000000..21ddf60 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/errors/ErrorConstants.java @@ -0,0 +1,17 @@ +package com.nayakfamily.familytree.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/nayakfamily/familytree/web/rest/errors/ExceptionTranslator.java b/src/main/java/com/nayakfamily/familytree/web/rest/errors/ExceptionTranslator.java new file mode 100644 index 0000000..cf74e51 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/errors/ExceptionTranslator.java @@ -0,0 +1,264 @@ +package com.nayakfamily.familytree.web.rest.errors; + +import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.URI; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +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.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.lang.Nullable; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.web.ErrorResponse; +import org.springframework.web.ErrorResponseException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import tech.jhipster.config.JHipsterConstants; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder; +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 extends ResponseEntityExceptionHandler { + + 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 boolean CASUAL_CHAIN_ENABLED = false; + + @Value("${jhipster.clientApp.name}") + private String applicationName; + + private final Environment env; + + public ExceptionTranslator(Environment env) { + this.env = env; + } + + @ExceptionHandler + public ResponseEntity handleAnyException(Throwable ex, NativeWebRequest request) { + ProblemDetailWithCause pdCause = wrapAndCustomizeProblem(ex, request); + return handleExceptionInternal((Exception) ex, pdCause, buildHeaders(ex), HttpStatusCode.valueOf(pdCause.getStatus()), request); + } + + @Nullable + @Override + protected ResponseEntity handleExceptionInternal( + Exception ex, + @Nullable Object body, + HttpHeaders headers, + HttpStatusCode statusCode, + WebRequest request + ) { + body = body == null ? wrapAndCustomizeProblem((Throwable) ex, (NativeWebRequest) request) : body; + return super.handleExceptionInternal(ex, body, headers, statusCode, request); + } + + protected ProblemDetailWithCause wrapAndCustomizeProblem(Throwable ex, NativeWebRequest request) { + return customizeProblem(getProblemDetailWithCause(ex), ex, request); + } + + private ProblemDetailWithCause getProblemDetailWithCause(Throwable ex) { + if ( + ex instanceof com.nayakfamily.familytree.service.UsernameAlreadyUsedException + ) return (ProblemDetailWithCause) new LoginAlreadyUsedException().getBody(); + if ( + ex instanceof com.nayakfamily.familytree.service.EmailAlreadyUsedException + ) return (ProblemDetailWithCause) new EmailAlreadyUsedException().getBody(); + if ( + ex instanceof com.nayakfamily.familytree.service.InvalidPasswordException + ) return (ProblemDetailWithCause) new InvalidPasswordException().getBody(); + + if ( + ex instanceof ErrorResponseException exp && exp.getBody() instanceof ProblemDetailWithCause problemDetailWithCause + ) return problemDetailWithCause; + return ProblemDetailWithCauseBuilder.instance().withStatus(toStatus(ex).value()).build(); + } + + protected ProblemDetailWithCause customizeProblem(ProblemDetailWithCause problem, Throwable err, NativeWebRequest request) { + if (problem.getStatus() <= 0) problem.setStatus(toStatus(err)); + + if (problem.getType() == null || problem.getType().equals(URI.create("about:blank"))) problem.setType(getMappedType(err)); + + // higher precedence to Custom/ResponseStatus types + String title = extractTitle(err, problem.getStatus()); + String problemTitle = problem.getTitle(); + if (problemTitle == null || !problemTitle.equals(title)) { + problem.setTitle(title); + } + + if (problem.getDetail() == null) { + // higher precedence to cause + problem.setDetail(getCustomizedErrorDetails(err)); + } + + Map problemProperties = problem.getProperties(); + if (problemProperties == null || !problemProperties.containsKey(MESSAGE_KEY)) problem.setProperty( + MESSAGE_KEY, + getMappedMessageKey(err) != null ? getMappedMessageKey(err) : "error.http." + problem.getStatus() + ); + + if (problemProperties == null || !problemProperties.containsKey(PATH_KEY)) problem.setProperty(PATH_KEY, getPathValue(request)); + + if ( + (err instanceof MethodArgumentNotValidException fieldException) && + (problemProperties == null || !problemProperties.containsKey(FIELD_ERRORS_KEY)) + ) problem.setProperty(FIELD_ERRORS_KEY, getFieldErrors(fieldException)); + + problem.setCause(buildCause(err.getCause(), request).orElse(null)); + + return problem; + } + + private String extractTitle(Throwable err, int statusCode) { + return getCustomizedTitle(err) != null ? getCustomizedTitle(err) : extractTitleForResponseStatus(err, statusCode); + } + + private List getFieldErrors(MethodArgumentNotValidException ex) { + return ex + .getBindingResult() + .getFieldErrors() + .stream() + .map( + f -> + new FieldErrorVM( + f.getObjectName().replaceFirst("DTO$", ""), + f.getField(), + StringUtils.isNotBlank(f.getDefaultMessage()) ? f.getDefaultMessage() : f.getCode() + ) + ) + .toList(); + } + + private String extractTitleForResponseStatus(Throwable err, int statusCode) { + ResponseStatus specialStatus = extractResponseStatus(err); + return specialStatus == null ? HttpStatus.valueOf(statusCode).getReasonPhrase() : specialStatus.reason(); + } + + private String extractURI(NativeWebRequest request) { + HttpServletRequest nativeRequest = request.getNativeRequest(HttpServletRequest.class); + return nativeRequest != null ? nativeRequest.getRequestURI() : StringUtils.EMPTY; + } + + private HttpStatus toStatus(final Throwable throwable) { + // Let the ErrorResponse take this responsibility + if (throwable instanceof ErrorResponse err) return HttpStatus.valueOf(err.getBody().getStatus()); + + return Optional.ofNullable(getMappedStatus(throwable)).orElse( + Optional.ofNullable(resolveResponseStatus(throwable)).map(ResponseStatus::value).orElse(HttpStatus.INTERNAL_SERVER_ERROR) + ); + } + + private ResponseStatus extractResponseStatus(final Throwable throwable) { + return Optional.ofNullable(resolveResponseStatus(throwable)).orElse(null); + } + + private ResponseStatus resolveResponseStatus(final Throwable type) { + final ResponseStatus candidate = findMergedAnnotation(type.getClass(), ResponseStatus.class); + return candidate == null && type.getCause() != null ? resolveResponseStatus(type.getCause()) : candidate; + } + + private URI getMappedType(Throwable err) { + if (err instanceof MethodArgumentNotValidException) return ErrorConstants.CONSTRAINT_VIOLATION_TYPE; + return ErrorConstants.DEFAULT_TYPE; + } + + private String getMappedMessageKey(Throwable err) { + if (err instanceof MethodArgumentNotValidException) { + return ErrorConstants.ERR_VALIDATION; + } else if (err instanceof ConcurrencyFailureException || err.getCause() instanceof ConcurrencyFailureException) { + return ErrorConstants.ERR_CONCURRENCY_FAILURE; + } + return null; + } + + private String getCustomizedTitle(Throwable err) { + if (err instanceof MethodArgumentNotValidException) return "Method argument not valid"; + return null; + } + + private String getCustomizedErrorDetails(Throwable err) { + Collection activeProfiles = Arrays.asList(env.getActiveProfiles()); + if (activeProfiles.contains(JHipsterConstants.SPRING_PROFILE_PRODUCTION)) { + if (err instanceof HttpMessageConversionException) return "Unable to convert http message"; + if (err instanceof DataAccessException) return "Failure during data access"; + if (containsPackageName(err.getMessage())) return "Unexpected runtime exception"; + } + return err.getCause() != null ? err.getCause().getMessage() : err.getMessage(); + } + + private HttpStatus getMappedStatus(Throwable err) { + // Where we disagree with Spring defaults + if (err instanceof AccessDeniedException) return HttpStatus.FORBIDDEN; + if (err instanceof ConcurrencyFailureException) return HttpStatus.CONFLICT; + if (err instanceof BadCredentialsException) return HttpStatus.UNAUTHORIZED; + return null; + } + + private URI getPathValue(NativeWebRequest request) { + if (request == null) return URI.create("about:blank"); + return URI.create(extractURI(request)); + } + + private HttpHeaders buildHeaders(Throwable err) { + return err instanceof BadRequestAlertException badRequestAlertException + ? HeaderUtil.createFailureAlert( + applicationName, + true, + badRequestAlertException.getEntityName(), + badRequestAlertException.getErrorKey(), + badRequestAlertException.getMessage() + ) + : null; + } + + public Optional buildCause(final Throwable throwable, NativeWebRequest request) { + if (throwable != null && isCasualChainEnabled()) { + return Optional.of(customizeProblem(getProblemDetailWithCause(throwable), throwable, request)); + } + return Optional.ofNullable(null); + } + + private boolean isCasualChainEnabled() { + // Customize as per the needs + return CASUAL_CHAIN_ENABLED; + } + + private boolean containsPackageName(String message) { + // This list is for sure not complete + return StringUtils.containsAny( + message, + "org.", + "java.", + "net.", + "jakarta.", + "javax.", + "com.", + "io.", + "de.", + "com.nayakfamily.familytree" + ); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/web/rest/errors/FieldErrorVM.java b/src/main/java/com/nayakfamily/familytree/web/rest/errors/FieldErrorVM.java new file mode 100644 index 0000000..2f5ac6b --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/errors/FieldErrorVM.java @@ -0,0 +1,32 @@ +package com.nayakfamily.familytree.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/nayakfamily/familytree/web/rest/errors/InvalidPasswordException.java b/src/main/java/com/nayakfamily/familytree/web/rest/errors/InvalidPasswordException.java new file mode 100644 index 0000000..955b00b --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/errors/InvalidPasswordException.java @@ -0,0 +1,23 @@ +package com.nayakfamily.familytree.web.rest.errors; + +import org.springframework.http.HttpStatus; +import org.springframework.web.ErrorResponseException; +import tech.jhipster.web.rest.errors.ProblemDetailWithCause.ProblemDetailWithCauseBuilder; + +@SuppressWarnings("java:S110") // Inheritance tree of classes should not be too deep +public class InvalidPasswordException extends ErrorResponseException { + + private static final long serialVersionUID = 1L; + + public InvalidPasswordException() { + super( + HttpStatus.BAD_REQUEST, + ProblemDetailWithCauseBuilder.instance() + .withStatus(HttpStatus.BAD_REQUEST.value()) + .withType(ErrorConstants.INVALID_PASSWORD_TYPE) + .withTitle("Incorrect password") + .build(), + null + ); + } +} diff --git a/src/main/java/com/nayakfamily/familytree/web/rest/errors/LoginAlreadyUsedException.java b/src/main/java/com/nayakfamily/familytree/web/rest/errors/LoginAlreadyUsedException.java new file mode 100644 index 0000000..ff84786 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/errors/LoginAlreadyUsedException.java @@ -0,0 +1,11 @@ +package com.nayakfamily.familytree.web.rest.errors; + +@SuppressWarnings("java:S110") // Inheritance tree of classes should not be too deep +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/nayakfamily/familytree/web/rest/errors/package-info.java b/src/main/java/com/nayakfamily/familytree/web/rest/errors/package-info.java new file mode 100644 index 0000000..5344e79 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/errors/package-info.java @@ -0,0 +1,4 @@ +/** + * Rest layer error handling. + */ +package com.nayakfamily.familytree.web.rest.errors; diff --git a/src/main/java/com/nayakfamily/familytree/web/rest/package-info.java b/src/main/java/com/nayakfamily/familytree/web/rest/package-info.java new file mode 100644 index 0000000..5622747 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/package-info.java @@ -0,0 +1,4 @@ +/** + * Rest layer. + */ +package com.nayakfamily.familytree.web.rest; diff --git a/src/main/java/com/nayakfamily/familytree/web/rest/vm/KeyAndPasswordVM.java b/src/main/java/com/nayakfamily/familytree/web/rest/vm/KeyAndPasswordVM.java new file mode 100644 index 0000000..2b3af9e --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/vm/KeyAndPasswordVM.java @@ -0,0 +1,27 @@ +package com.nayakfamily.familytree.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/nayakfamily/familytree/web/rest/vm/LoginVM.java b/src/main/java/com/nayakfamily/familytree/web/rest/vm/LoginVM.java new file mode 100644 index 0000000..886d97a --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/vm/LoginVM.java @@ -0,0 +1,53 @@ +package com.nayakfamily.familytree.web.rest.vm; + +import jakarta.validation.constraints.NotNull; +import jakarta.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/nayakfamily/familytree/web/rest/vm/ManagedUserVM.java b/src/main/java/com/nayakfamily/familytree/web/rest/vm/ManagedUserVM.java new file mode 100644 index 0000000..53f7910 --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/vm/ManagedUserVM.java @@ -0,0 +1,35 @@ +package com.nayakfamily.familytree.web.rest.vm; + +import com.nayakfamily.familytree.service.dto.AdminUserDTO; +import jakarta.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/nayakfamily/familytree/web/rest/vm/package-info.java b/src/main/java/com/nayakfamily/familytree/web/rest/vm/package-info.java new file mode 100644 index 0000000..05e425e --- /dev/null +++ b/src/main/java/com/nayakfamily/familytree/web/rest/vm/package-info.java @@ -0,0 +1,4 @@ +/** + * Rest layer visual models. + */ +package com.nayakfamily.familytree.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..b2122ea --- /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\:./target/h2db/db/nayaksfamily|nayaksFamily +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..5e65537 --- /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} :: Startup profile(s) ${spring.profiles.active} :: +:: 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..f5a4f25 --- /dev/null +++ b/src/main/resources/config/application-dev.yml @@ -0,0 +1,109 @@ +# =================================================================== +# 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.nayakfamily.familytree: 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:./target/h2db/db/nayaksFamily;DB_CLOSE_DELAY=-1 + username: nayaksFamily + password: + hikari: + poolName: Hikari + auto-commit: false + h2: + console: + # disable spring boot built-in h2-console since we start it manually with correct configuration + enabled: false + 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,https://localhost:8100,http://localhost:9000,https://localhost:9000,http://localhost:9060,https://localhost:9060' + # Enable CORS when running in GitHub Codespaces + allowed-origin-patterns: 'https://*.githubpreview.dev' + 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: YmVlMWZiNDdiNzQwN2Q4YTMyNjBhZTU3ZjQ1YjJkMDUyMDBiMGRiYzQ2ZDc1ZmMxNWQzYTA3MzYyNWE3ZWExOWM0YjJhMDQ4NmQ4ZmUyNTA2ZThkYjI2MWE4YTMwMjBjZjI2NjMwNzE1MGRiNGQ2ZDEwNmYzN2U4NmFmZDQwNmU= + # 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 + ring-buffer-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..ef056a2 --- /dev/null +++ b/src/main/resources/config/application-prod.yml @@ -0,0 +1,124 @@ +# =================================================================== +# 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.nayakfamily.familytree: INFO + +management: + prometheus: + metrics: + export: + enabled: false + +spring: + devtools: + restart: + enabled: false + livereload: + enabled: false + datasource: + type: com.zaxxer.hikari.HikariDataSource + url: jdbc:postgresql://localhost:5432/nayaksFamily + username: nayaksFamily + password: + hikari: + poolName: Hikari + auto-commit: false + # 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 nayaksfamily -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 3650 +# +# You can also use Let's Encrypt: +# See details in topic "Create a Java Keystore (.JKS) from Let's Encrypt Certificates" on https://maximilian-boehm.com/en-gb/blog +# +# 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,image/svg+xml + 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 Consul configserver + # - 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: YmVlMWZiNDdiNzQwN2Q4YTMyNjBhZTU3ZjQ1YjJkMDUyMDBiMGRiYzQ2ZDc1ZmMxNWQzYTA3MzYyNWE3ZWExOWM0YjJhMDQ4NmQ4ZmUyNTA2ZThkYjI2MWE4YTMwMjBjZjI2NjMwNzE1MGRiNGQ2ZDEwNmYzN2U4NmFmZDQwNmU= + # 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 + ring-buffer-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..039f6f4 --- /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_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 + 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..50aca63 --- /dev/null +++ b/src/main/resources/config/application.yml @@ -0,0 +1,213 @@ +# =================================================================== +# 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 +# =================================================================== + +--- +# Conditionally disable springdoc on missing api-docs profile +spring: + config: + activate: + on-profile: '!api-docs' +springdoc: + api-docs: + enabled: false +--- +management: + endpoints: + web: + base-path: /management + exposure: + include: + - configprops + - env + - health + - info + - jhimetrics + - jhiopenapigroups + - logfile + - loggers + - prometheus + - threaddump + - caches + - liquibase + endpoint: + health: + show-details: when_authorized + roles: 'ROLE_ADMIN' + probes: + enabled: true + group: + liveness: + include: livenessState + readiness: + include: readinessState,db + jhimetrics: + enabled: true + info: + git: + mode: full + env: + enabled: true + health: + mail: + enabled: false # When using the MailService, configure an SMTP server and set this to true + prometheus: + metrics: + export: + enabled: true + step: 60 + observations: + key-values: + application: ${spring.application.name} + metrics: + 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 + data: + repository: + autotime: + enabled: true + +spring: + application: + name: nayaksFamily + profiles: + # The commented value for `active` can be replaced with valid Spring profiles to load. + # Otherwise, it will be filled in by maven 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.timezone.default_storage: NORMALIZE + hibernate.type.preferred_instant_jdbc_type: TIMESTAMP + 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.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy + implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy + messages: + basename: i18n/messages + main: + allow-bean-definition-overriding: true + mvc: + problemdetails: + enabled: true + task: + execution: + thread-name-prefix: nayaks-family-task- + pool: + core-size: 2 + max-size: 50 + queue-capacity: 10000 + scheduling: + thread-name-prefix: nayaks-family-scheduling- + pool: + size: 2 + thymeleaf: + mode: HTML + output: + ansi: + console-available: true + +server: + servlet: + session: + cookie: + http-only: true + +springdoc: + show-actuator: 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: 'nayaksFamilyApp' + # By default CORS is disabled. Uncomment to enable. + # cors: + # allowed-origins: "http://localhost:8100,http://localhost:9000" + # 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: nayaksFamily@localhost + api-docs: + default-include-pattern: /api/** + management-include-pattern: /management/** + title: Nayaks Family API + description: Nayaks Family API documentation + version: 0.0.1 + terms-of-service-url: + contact-name: + contact-url: + contact-email: + license: unlicensed + license-url: + security: + content-security-policy: "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:" +# =================================================================== +# 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..5f42c30 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/master.xml b/src/main/resources/config/liquibase/master.xml new file mode 100644 index 0000000..c03e904 --- /dev/null +++ b/src/main/resources/config/liquibase/master.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/tls/keystore.p12 b/src/main/resources/config/tls/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..096e7e4e49ecc0bb309cf1e676760bca56d85b56 GIT binary patch literal 2768 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 ti>N{87bmrZd|-Z}vh1N8Fx; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html new file mode 100644 index 0000000..31ee9d7 --- /dev/null +++ b/src/main/resources/templates/error.html @@ -0,0 +1,94 @@ + + + + + + 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..6be1e4f --- /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..ab46621 --- /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..eb74196 --- /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..3202d9a --- /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..8aeabad --- /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: #ffffff; diff --git a/src/main/webapp/app/app.scss b/src/main/webapp/app/app.scss new file mode 100644 index 0000000..58ae6d1 --- /dev/null +++ b/src/main/webapp/app/app.scss @@ -0,0 +1,315 @@ +// Override Bootstrap variables +@import 'bootstrap-variables'; +// Import Bootstrap source files from node_modules +@import '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..d84df29 --- /dev/null +++ b/src/main/webapp/app/app.tsx @@ -0,0 +1,63 @@ +import 'react-toastify/dist/ReactToastify.css'; +import './app.scss'; +import 'app/config/dayjs'; + +import React, { useEffect } from 'react'; +import { Card } from 'reactstrap'; +import { BrowserRouter } from 'react-router-dom'; +import { ToastContainer, toast } from 'react-toastify'; + +import { useAppDispatch, useAppSelector } from 'app/config/store'; +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 const App = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(getSession()); + dispatch(getProfile()); + }, []); + + const isAuthenticated = useAppSelector(state => state.authentication.isAuthenticated); + const isAdmin = useAppSelector(state => hasAnyAuthority(state.authentication.account.authorities, [AUTHORITIES.ADMIN])); + const ribbonEnv = useAppSelector(state => state.applicationProfile.ribbonEnv); + const isInProduction = useAppSelector(state => state.applicationProfile.inProduction); + const isOpenAPIEnabled = useAppSelector(state => state.applicationProfile.isOpenAPIEnabled); + + const paddingTop = '60px'; + return ( + +
+ + +
+ +
+ + + + + +
+
+
+
+ ); +}; + +export default 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..5d6546b --- /dev/null +++ b/src/main/webapp/app/config/axios-interceptor.spec.ts @@ -0,0 +1,32 @@ +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', () => { + const rejectError = { + response: { + statusText: 'NotFound', + status: 403, + data: { message: 'Page not found' }, + }, + }; + expect((client.interceptors.response as any).handlers[0].rejected(rejectError)).rejects.toEqual(rejectError); + 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..f529b27 --- /dev/null +++ b/src/main/webapp/app/config/axios-interceptor.ts @@ -0,0 +1,28 @@ +import axios from 'axios'; +import { Storage } from 'react-jhipster'; + +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..bd0e4c8 --- /dev/null +++ b/src/main/webapp/app/config/constants.ts @@ -0,0 +1,15 @@ +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_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/error-middleware.ts b/src/main/webapp/app/config/error-middleware.ts new file mode 100644 index 0000000..ecf1d5e --- /dev/null +++ b/src/main/webapp/app/config/error-middleware.ts @@ -0,0 +1,29 @@ +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 => { + /** + * + * The error middleware serves to log error messages from dispatch + * It need not run in production + */ + if (DEVELOPMENT) { + const { error } = action; + if (error) { + console.error(`${action.type} caught at middleware with reason: ${JSON.stringify(error.message)}.`); + if (error.response && error.response.data) { + const message = getErrorMessage(error.response.data); + console.error(`Actual cause: ${message}`); + } + } + } + // Dispatch initial action + 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..2d1b244 --- /dev/null +++ b/src/main/webapp/app/config/icon-loader.ts @@ -0,0 +1,75 @@ +import { + faCogs, + faBan, + faAsterisk, + faArrowLeft, + faBell, + faBook, + faCloud, + faDatabase, + faEye, + faFlag, + faHeart, + faHome, + faList, + faLock, + faPencilAlt, + faPlus, + faSave, + faSearch, + faSort, + faSync, + faRoad, + faSignInAlt, + faSignOutAlt, + faTachometerAlt, + faTasks, + faThList, + faTimesCircle, + faTrash, + faUser, + faUserPlus, + faUsers, + faUsersCog, + faWrench, +} from '@fortawesome/free-solid-svg-icons'; + +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..013003e --- /dev/null +++ b/src/main/webapp/app/config/logger-middleware.ts @@ -0,0 +1,16 @@ +/* eslint no-console: off */ +export default () => next => action => { + if (DEVELOPMENT) { + const { type, payload, meta, error } = action; + + console.groupCollapsed(type); + console.log('Payload:', payload); + if (error) { + console.log('Error:', error); + } + 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..2b0239b --- /dev/null +++ b/src/main/webapp/app/config/notification-middleware.spec.ts @@ -0,0 +1,241 @@ +import { createStore, applyMiddleware } from 'redux'; +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/fulfilled'; + const ERROR_TYPE = 'ERROR/rejected'; + + // Default action for use in local tests + const DEFAULT = { + type: SUCCESS_TYPE, + payload: 'foo', + }; + const HEADER_SUCCESS = { + type: SUCCESS_TYPE, + payload: { + status: 201, + statusText: 'Created', + headers: { 'app-alert': 'foo.created', 'app-params': 'foo' }, + }, + }; + + const DEFAULT_ERROR = { + type: ERROR_TYPE, + error: new Error('foo'), + }; + const VALIDATION_ERROR = { + type: ERROR_TYPE, + error: { + isAxiosError: true, + 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, + error: { + isAxiosError: true, + response: { + status: 400, + statusText: 'Bad Request', + headers: { 'app-error': 'foo.creation', 'app-params': 'foo' }, + }, + }, + }; + const NOT_FOUND_ERROR = { + type: ERROR_TYPE, + error: { + isAxiosError: true, + response: { + data: { + status: 404, + message: 'Not found', + }, + status: 404, + }, + }, + }; + const NO_SERVER_ERROR = { + type: ERROR_TYPE, + error: { + isAxiosError: true, + response: { + status: 0, + }, + }, + }; + const GENERIC_ERROR = { + type: ERROR_TYPE, + error: { + isAxiosError: true, + response: { + data: { + message: 'Error', + }, + }, + }, + }; + const LOGIN_REJECTED_ERROR = { + type: ERROR_TYPE, + error: { + isAxiosError: true, + response: { + data: '', + config: { + url: 'api/authenticate', + }, + status: 401, + }, + }, + }; + + const TITLE_ERROR = { + type: ERROR_TYPE, + error: { + isAxiosError: true, + response: { + data: { + title: 'Incorrect password', + status: 400, + type: 'https://www.jhipster.tech/problem/invalid-password', + }, + status: 400, + }, + }, + }; + + const STRING_DATA_ERROR = { + type: ERROR_TYPE, + error: { + isAxiosError: true, + response: { + data: 'Incorrect password string', + status: 400, + }, + }, + }; + + const UNKNOWN_400_ERROR = { + type: ERROR_TYPE, + error: { + isAxiosError: true, + response: { + status: 400, + }, + }, + }; + + const UNKNOWN_ERROR = { + type: ERROR_TYPE, + error: { + isAxiosError: true, + }, + }; + + const makeStore = () => applyMiddleware(notificationMiddleware)(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 trigger a success toast message for header alerts', () => { + expect(store.dispatch(HEADER_SUCCESS).payload.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 error', () => { + expect(store.dispatch(DEFAULT_ERROR).error.message).toEqual('foo'); + const toastMsg = (toastify.toast as any).error.getCall(0).args[0]; + expect(toastMsg).toEqual('foo'); + }); + + it('should trigger an error toast message and return error for generic message', () => { + expect(store.dispatch(GENERIC_ERROR).error.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 error for 400 response code', () => { + expect(store.dispatch(VALIDATION_ERROR).error.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 error for 404 response code', () => { + expect(store.dispatch(NOT_FOUND_ERROR).error.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 error for 0 response code', () => { + expect(store.dispatch(NO_SERVER_ERROR).error.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 error for headers containing errors', () => { + expect(store.dispatch(HEADER_ERRORS).error.response.status).toEqual(400); + const toastMsg = (toastify.toast as any).error.getCall(0).args[0]; + expect(toastMsg).toContain('foo.creation'); + }); + + it('should not trigger an error toast message and return error for 401 response code', () => { + expect(store.dispatch(LOGIN_REJECTED_ERROR).error.response.status).toEqual(401); + expect((toastify.toast as any).error.called).toEqual(false); + expect((toastify.toast as any).success.called).toEqual(false); + }); + + it('should trigger an error toast message and return error for 400 response code', () => { + expect(store.dispatch(TITLE_ERROR).error.response.status).toEqual(400); + const toastMsg = (toastify.toast as any).error.getCall(0).args[0]; + expect(toastMsg).toContain('Incorrect password'); + }); + + it('should trigger an error toast message and return error for string in data', () => { + expect(store.dispatch(STRING_DATA_ERROR).error.response.status).toEqual(400); + const toastMsg = (toastify.toast as any).error.getCall(0).args[0]; + expect(toastMsg).toContain('Incorrect password string'); + }); + + it('should trigger an error toast message and return error for unknown 400 error', () => { + expect(store.dispatch(UNKNOWN_400_ERROR).error.response.status).toEqual(400); + const toastMsg = (toastify.toast as any).error.getCall(0).args[0]; + expect(toastMsg).toContain('Unknown error!'); + }); + + it('should trigger an error toast message and return error for unknown error', () => { + expect(store.dispatch(UNKNOWN_ERROR).error.isAxiosError).toEqual(true); + const toastMsg = (toastify.toast as any).error.getCall(0).args[0]; + expect(toastMsg).toContain('Unknown error!'); + }); +}); 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..9b6b9f2 --- /dev/null +++ b/src/main/webapp/app/config/notification-middleware.ts @@ -0,0 +1,102 @@ +import { toast } from 'react-toastify'; +import { isFulfilledAction, isRejectedAction } from 'app/shared/reducers/reducer.utils'; +import { AxiosError, AxiosHeaderValue } from 'axios'; + +const addErrorAlert = (message, key?, data?) => { + toast.error(message); +}; + +export default () => next => action => { + const { error, payload } = action; + + /** + * + * The notification middleware serves to add success and error notifications + */ + if (isFulfilledAction(action) && payload && payload.headers) { + const headers = payload?.headers; + let alert: string | null = null; + headers && + Object.entries(headers).forEach(([k, v]) => { + if (k.toLowerCase().endsWith('app-alert')) { + alert = v; + } + }); + if (alert) { + toast.success(alert); + } + } + + if (isRejectedAction(action) && error && error.isAxiosError) { + const axiosError = error as AxiosError; + if (axiosError.response) { + const response = axiosError.response; + const data = response.data as any; + if ( + !( + response.status === 401 && + (axiosError.message === '' || response.config.url === 'api/account' || response.config.url === 'api/authenticate') + ) + ) { + switch (response.status) { + // connection refused, server not reachable + case 0: + addErrorAlert('Server not reachable', 'error.server.not.reachable'); + break; + + case 400: { + let errorHeader: string | null = null; + let entityKey: string | null = null; + response?.headers && + Object.entries(response.headers).forEach(([k, v]) => { + if (k.toLowerCase().endsWith('app-error')) { + errorHeader = v as string; + } else if (k.toLowerCase().endsWith('app-params')) { + entityKey = v as string; + } + }); + if (errorHeader) { + const entityName = entityKey; + addErrorAlert(errorHeader, errorHeader, { entityName }); + } else if (data?.fieldErrors) { + const fieldErrors = data.fieldErrors; + for (const fieldError of fieldErrors) { + 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 (typeof data === 'string' && data !== '') { + addErrorAlert(data); + } else { + toast.error(data?.message || data?.error || data?.title || 'Unknown error!'); + } + break; + } + case 404: + addErrorAlert('Not found', 'error.url.not.found'); + break; + + default: + if (typeof data === 'string' && data !== '') { + addErrorAlert(data); + } else { + toast.error(data?.message || data?.error || data?.title || 'Unknown error!'); + } + } + } + } else if (axiosError.config && axiosError.config.url === 'api/account' && axiosError.config.method === 'get') { + /* eslint-disable no-console */ + console.log('Authentication Error: Trying to access url api/account with GET.'); + } else { + toast.error(error.message || 'Unknown error!'); + } + } else if (error) { + toast.error(error.message || 'Unknown error!'); + } + + return next(action); +}; diff --git a/src/main/webapp/app/config/store.ts b/src/main/webapp/app/config/store.ts new file mode 100644 index 0000000..adb88d7 --- /dev/null +++ b/src/main/webapp/app/config/store.ts @@ -0,0 +1,30 @@ +import { UnknownAction, configureStore, ThunkAction } from '@reduxjs/toolkit'; +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import { loadingBarMiddleware } from 'react-redux-loading-bar'; + +import sharedReducers from 'app/shared/reducers'; +import errorMiddleware from './error-middleware'; +import notificationMiddleware from './notification-middleware'; +import loggerMiddleware from './logger-middleware'; + +const store = configureStore({ + reducer: sharedReducers, + middleware: getDefaultMiddleware => + getDefaultMiddleware({ + serializableCheck: { + // Ignore these field paths in all actions + ignoredActionPaths: ['payload.config', 'payload.request', 'payload.headers', 'error', 'meta.arg'], + }, + }).concat(errorMiddleware, notificationMiddleware, loadingBarMiddleware(), loggerMiddleware), +}); + +const getStore = () => store; + +export type IRootState = ReturnType; +export type AppDispatch = typeof store.dispatch; + +export const useAppSelector: TypedUseSelectorHook = useSelector; +export const useAppDispatch = () => useDispatch(); +export type AppThunk = ThunkAction; + +export default getStore; diff --git a/src/main/webapp/app/entities/menu.tsx b/src/main/webapp/app/entities/menu.tsx new file mode 100644 index 0000000..56b6309 --- /dev/null +++ b/src/main/webapp/app/entities/menu.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import MenuItem from 'app/shared/layout/menus/menu-item'; + +const EntitiesMenu = () => { + return ( + <> + {/* prettier-ignore */} + {/* jhipster-needle-add-entity-to-menu - JHipster will add entities to the menu here */} + + ); +}; + +export default EntitiesMenu; diff --git a/src/main/webapp/app/entities/reducers.ts b/src/main/webapp/app/entities/reducers.ts new file mode 100644 index 0000000..efad4f9 --- /dev/null +++ b/src/main/webapp/app/entities/reducers.ts @@ -0,0 +1,7 @@ +/* jhipster-needle-add-reducer-import - JHipster will add reducer here */ + +const entitiesReducers = { + /* jhipster-needle-add-reducer-combine - JHipster will add reducer here */ +}; + +export default entitiesReducers; diff --git a/src/main/webapp/app/entities/routes.tsx b/src/main/webapp/app/entities/routes.tsx new file mode 100644 index 0000000..84e142c --- /dev/null +++ b/src/main/webapp/app/entities/routes.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Route } from 'react-router-dom'; + +import ErrorBoundaryRoutes from 'app/shared/error/error-boundary-routes'; + +/* jhipster-needle-add-route-import - JHipster will add routes here */ + +export default () => { + return ( +
+ + {/* prettier-ignore */} + {/* jhipster-needle-add-route-path - JHipster will add routes here */} + +
+ ); +}; diff --git a/src/main/webapp/app/index.tsx b/src/main/webapp/app/index.tsx new file mode 100644 index 0000000..e1d2b2e --- /dev/null +++ b/src/main/webapp/app/index.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { Provider } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import getStore from 'app/config/store'; +import setupAxiosInterceptors from 'app/config/axios-interceptor'; +import { clearAuthentication } from 'app/shared/reducers/authentication'; +import ErrorBoundary from 'app/shared/error/error-boundary'; +import AppComponent from 'app/app'; +import { loadIcons } from 'app/config/icon-loader'; + +const store = getStore(); + +const actions = bindActionCreators({ clearAuthentication }, store.dispatch); +setupAxiosInterceptors(() => actions.clearAuthentication('login.error.unauthorized')); + +loadIcons(); + +const rootEl = document.getElementById('root'); +const root = createRoot(rootEl); + +const render = Component => + root.render( + + +
+ +
+
+
, + ); + +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..bd63383 --- /dev/null +++ b/src/main/webapp/app/modules/account/activate/activate.reducer.spec.ts @@ -0,0 +1,82 @@ +import axios from 'axios'; +import sinon from 'sinon'; +import { configureStore } from '@reduxjs/toolkit'; + +import activate, { activateAction, reset } from './activate.reducer'; + +describe('Activate reducer tests', () => { + it('should return the initial state', () => { + expect(activate(undefined, { type: '' })).toMatchObject({ + activationSuccess: false, + activationFailure: false, + }); + }); + + it('should reset', () => { + expect(activate({ activationSuccess: true, activationFailure: false }, reset)).toMatchObject({ + activationSuccess: false, + activationFailure: false, + }); + }); + + it('should detect a success', () => { + expect(activate(undefined, { type: activateAction.fulfilled.type })).toMatchObject({ + activationSuccess: true, + activationFailure: false, + }); + }); + + it('should return the same state on request', () => { + expect(activate(undefined, { type: activateAction.pending.type })).toMatchObject({ + activationSuccess: false, + activationFailure: false, + }); + }); + + it('should detect a failure', () => { + expect(activate(undefined, { type: activateAction.rejected.type })).toMatchObject({ + activationSuccess: false, + activationFailure: true, + }); + }); + + it('should reset the state', () => { + const initialState = { + activationSuccess: false, + activationFailure: false, + }; + expect(activate({ activationSuccess: true, activationFailure: true }, reset)).toEqual({ + ...initialState, + }); + }); + + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + const getState = jest.fn(); + const dispatch = jest.fn(); + const extra = {}; + beforeEach(() => { + store = configureStore({ + reducer: (state = [], action) => [...state, action], + }); + axios.get = sinon.stub().returns(Promise.resolve(resolvedObject)); + }); + + it('dispatches ACTIVATE_ACCOUNT_PENDING and ACTIVATE_ACCOUNT_FULFILLED actions', async () => { + const arg = ''; + + const result = await activateAction(arg)(dispatch, getState, extra); + + const pendingAction = dispatch.mock.calls[0][0]; + expect(pendingAction.meta.requestStatus).toBe('pending'); + expect(activateAction.fulfilled.match(result)).toBe(true); + expect(result.payload).toBe(resolvedObject); + }); + it('dispatches RESET actions', async () => { + await store.dispatch(reset()); + expect(store.getState()).toEqual([expect.any(Object), expect.objectContaining(reset())]); + }); + }); +}); 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..aa8ebed --- /dev/null +++ b/src/main/webapp/app/modules/account/activate/activate.reducer.ts @@ -0,0 +1,42 @@ +import axios from 'axios'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; + +import { serializeAxiosError } from 'app/shared/reducers/reducer.utils'; + +const initialState = { + activationSuccess: false, + activationFailure: false, +}; + +export type ActivateState = Readonly; + +// Actions + +export const activateAction = createAsyncThunk('activate/activate_account', async (key: string) => axios.get(`api/activate?key=${key}`), { + serializeError: serializeAxiosError, +}); + +export const ActivateSlice = createSlice({ + name: 'activate', + initialState: initialState as ActivateState, + reducers: { + reset() { + return initialState; + }, + }, + extraReducers(builder) { + builder + .addCase(activateAction.pending, () => initialState) + .addCase(activateAction.rejected, state => { + state.activationFailure = true; + }) + .addCase(activateAction.fulfilled, state => { + state.activationSuccess = true; + }); + }, +}); + +export const { reset } = ActivateSlice.actions; + +// Reducer +export default ActivateSlice.reducer; 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..caf9002 --- /dev/null +++ b/src/main/webapp/app/modules/account/activate/activate.tsx @@ -0,0 +1,53 @@ +import React, { useEffect } from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; +import { Row, Col, Alert } from 'reactstrap'; + +import { useAppDispatch, useAppSelector } from 'app/config/store'; +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 const ActivatePage = () => { + const dispatch = useAppDispatch(); + + const [searchParams] = useSearchParams(); + + useEffect(() => { + const key = searchParams.get('key'); + + dispatch(activateAction(key)); + return () => { + dispatch(reset()); + }; + }, []); + + const { activationSuccess, activationFailure } = useAppSelector(state => state.activate); + + return ( +
+ + +

Activation

+ {activationSuccess ? successAlert : undefined} + {activationFailure ? failureAlert : undefined} + +
+
+ ); +}; + +export default 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..26ca6f3 --- /dev/null +++ b/src/main/webapp/app/modules/account/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Route } from 'react-router-dom'; + +import ErrorBoundaryRoutes from 'app/shared/error/error-boundary-routes'; + +import Settings from './settings/settings'; +import Password from './password/password'; + +const AccountRoutes = () => ( +
+ + } /> + } /> + +
+); + +export default AccountRoutes; 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..55d2ce7 --- /dev/null +++ b/src/main/webapp/app/modules/account/password-reset/finish/password-reset-finish.tsx @@ -0,0 +1,87 @@ +import React, { useState, useEffect } from 'react'; +import { Col, Row, Button } from 'reactstrap'; +import { ValidatedField, ValidatedForm } from 'react-jhipster'; +import { useSearchParams } from 'react-router-dom'; +import { toast } from 'react-toastify'; + +import { handlePasswordResetFinish, reset } from '../password-reset.reducer'; +import PasswordStrengthBar from 'app/shared/layout/password/password-strength-bar'; +import { useAppDispatch, useAppSelector } from 'app/config/store'; + +export const PasswordResetFinishPage = () => { + const dispatch = useAppDispatch(); + + const [searchParams] = useSearchParams(); + const key = searchParams.get('key'); + + const [password, setPassword] = useState(''); + + useEffect( + () => () => { + dispatch(reset()); + }, + [], + ); + + const handleValidSubmit = ({ newPassword }) => dispatch(handlePasswordResetFinish({ key, newPassword })); + + const updatePassword = event => setPassword(event.target.value); + + const getResetForm = () => { + return ( + + + + v === password || 'The password and its confirmation do not match!', + }} + data-cy="confirmResetPassword" + /> + + + ); + }; + + const successMessage = useAppSelector(state => state.passwordReset.successMessage); + + useEffect(() => { + if (successMessage) { + toast.success(successMessage); + } + }, [successMessage]); + + return ( +
+ + +

Reset password

+
{key ? getResetForm() : null}
+ +
+
+ ); +}; + +export default 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..c5f48c3 --- /dev/null +++ b/src/main/webapp/app/modules/account/password-reset/init/password-reset-init.tsx @@ -0,0 +1,63 @@ +import React, { useEffect } from 'react'; +import { ValidatedField, ValidatedForm, isEmail } from 'react-jhipster'; +import { Button, Alert, Col, Row } from 'reactstrap'; +import { toast } from 'react-toastify'; + +import { handlePasswordResetInit, reset } from '../password-reset.reducer'; +import { useAppDispatch, useAppSelector } from 'app/config/store'; + +export const PasswordResetInit = () => { + const dispatch = useAppDispatch(); + + useEffect( + () => () => { + dispatch(reset()); + }, + [], + ); + + const handleValidSubmit = ({ email }) => { + dispatch(handlePasswordResetInit(email)); + }; + + const successMessage = useAppSelector(state => state.passwordReset.successMessage); + + useEffect(() => { + if (successMessage) { + toast.success(successMessage); + } + }, [successMessage]); + + return ( +
+ + +

Reset your password

+ +

Enter the email address you used to register

+
+ + isEmail(v) || 'Your email is invalid.', + }} + data-cy="emailResetPassword" + /> + + + +
+
+ ); +}; + +export default 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..5d44601 --- /dev/null +++ b/src/main/webapp/app/modules/account/password-reset/password-reset.reducer.ts @@ -0,0 +1,67 @@ +import axios from 'axios'; +import { createAsyncThunk, createSlice, isPending, isRejected } from '@reduxjs/toolkit'; + +import { serializeAxiosError } from 'app/shared/reducers/reducer.utils'; + +const initialState = { + loading: false, + resetPasswordSuccess: false, + resetPasswordFailure: false, + successMessage: null, +}; + +export type PasswordResetState = Readonly; + +const apiUrl = 'api/account/reset-password'; +// Actions + +export const handlePasswordResetInit = createAsyncThunk( + 'passwordReset/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. + async (mail: string) => axios.post(`${apiUrl}/init`, mail, { headers: { ['Content-Type']: 'text/plain' } }), + { serializeError: serializeAxiosError }, +); + +export const handlePasswordResetFinish = createAsyncThunk( + 'passwordReset/reset_password_finish', + async (data: { key: string; newPassword: string }) => axios.post(`${apiUrl}/finish`, data), + { serializeError: serializeAxiosError }, +); + +export const PasswordResetSlice = createSlice({ + name: 'passwordReset', + initialState: initialState as PasswordResetState, + reducers: { + reset() { + return initialState; + }, + }, + extraReducers(builder) { + builder + .addCase(handlePasswordResetInit.fulfilled, () => ({ + ...initialState, + loading: false, + resetPasswordSuccess: true, + successMessage: 'Check your email for details on how to reset your password.', + })) + .addCase(handlePasswordResetFinish.fulfilled, () => ({ + ...initialState, + loading: false, + resetPasswordSuccess: true, + successMessage: "Your password couldn't be reset. Remember a password request is only valid for 24 hours.", + })) + .addMatcher(isPending(handlePasswordResetInit, handlePasswordResetFinish), state => { + state.loading = true; + }) + .addMatcher(isRejected(handlePasswordResetInit, handlePasswordResetFinish), () => ({ + ...initialState, + loading: false, + resetPasswordFailure: true, + })); + }, +}); + +export const { reset } = PasswordResetSlice.actions; + +// Reducer +export default PasswordResetSlice.reducer; 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..ecf3bc4 --- /dev/null +++ b/src/main/webapp/app/modules/account/password/password.reducer.spec.ts @@ -0,0 +1,88 @@ +import axios from 'axios'; +import sinon from 'sinon'; +import { configureStore } from '@reduxjs/toolkit'; + +import password, { savePassword, reset } from './password.reducer'; + +describe('Password reducer tests', () => { + describe('Common tests', () => { + it('should return the initial state', () => { + const toTest = password(undefined, { type: '' }); + expect(toTest).toMatchObject({ + loading: false, + errorMessage: null, + updateSuccess: false, + updateFailure: false, + }); + }); + }); + + describe('Password update', () => { + it('should detect a request', () => { + const toTest = password(undefined, { type: savePassword.pending.type }); + expect(toTest).toMatchObject({ + updateSuccess: false, + updateFailure: false, + loading: true, + }); + }); + it('should detect a success', () => { + const toTest = password(undefined, { type: savePassword.fulfilled.type }); + expect(toTest).toMatchObject({ + updateSuccess: true, + updateFailure: false, + loading: false, + }); + }); + it('should detect a failure', () => { + const toTest = password(undefined, { type: savePassword.rejected.type }); + expect(toTest).toMatchObject({ + updateSuccess: false, + updateFailure: true, + loading: false, + }); + }); + + it('should reset the state', () => { + const initialState = { + loading: false, + errorMessage: null, + successMessage: null, + updateSuccess: false, + updateFailure: false, + }; + expect(password({ ...initialState, loading: true }, reset)).toEqual({ + ...initialState, + }); + }); + }); + + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + const getState = jest.fn(); + const dispatch = jest.fn(); + const extra = {}; + beforeEach(() => { + store = configureStore({ + reducer: (state = [], action) => [...state, action], + }); + axios.post = sinon.stub().returns(Promise.resolve(resolvedObject)); + }); + + it('dispatches UPDATE_PASSWORD_PENDING and UPDATE_PASSWORD_FULFILLED actions', async () => { + const arg = { currentPassword: '', newPassword: '' }; + + const result = await savePassword(arg)(dispatch, getState, extra); + + const pendingAction = dispatch.mock.calls[0][0]; + expect(pendingAction.meta.requestStatus).toBe('pending'); + expect(savePassword.fulfilled.match(result)).toBe(true); + }); + it('dispatches RESET actions', async () => { + await store.dispatch(reset()); + expect(store.getState()).toEqual([expect.any(Object), expect.objectContaining(reset())]); + }); + }); +}); 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..c1ab622 --- /dev/null +++ b/src/main/webapp/app/modules/account/password/password.reducer.ts @@ -0,0 +1,64 @@ +import axios from 'axios'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; + +import { serializeAxiosError } from 'app/shared/reducers/reducer.utils'; + +const initialState = { + loading: false, + errorMessage: null, + successMessage: null, + updateSuccess: false, + updateFailure: false, +}; + +export type PasswordState = Readonly; + +const apiUrl = 'api/account'; + +interface IPassword { + currentPassword: string; + newPassword: string; +} + +// Actions + +export const savePassword = createAsyncThunk( + 'password/update_password', + async (password: IPassword) => axios.post(`${apiUrl}/change-password`, password), + { serializeError: serializeAxiosError }, +); + +export const PasswordSlice = createSlice({ + name: 'password', + initialState: initialState as PasswordState, + reducers: { + reset() { + return initialState; + }, + }, + extraReducers(builder) { + builder + .addCase(savePassword.pending, state => { + state.errorMessage = null; + state.updateSuccess = false; + state.loading = true; + }) + .addCase(savePassword.rejected, state => { + state.loading = false; + state.updateSuccess = false; + state.updateFailure = true; + state.errorMessage = 'An error has occurred! The password could not be changed.'; + }) + .addCase(savePassword.fulfilled, state => { + state.loading = false; + state.updateSuccess = true; + state.updateFailure = false; + state.successMessage = 'Password changed!'; + }); + }, +}); + +export const { reset } = PasswordSlice.actions; + +// Reducer +export default PasswordSlice.reducer; 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..d7e664a --- /dev/null +++ b/src/main/webapp/app/modules/account/password/password.tsx @@ -0,0 +1,97 @@ +import React, { useState, useEffect } from 'react'; +import { ValidatedField, ValidatedForm } from 'react-jhipster'; +import { Row, Col, Button } from 'reactstrap'; +import { toast } from 'react-toastify'; + +import { useAppDispatch, useAppSelector } from 'app/config/store'; +import { getSession } from 'app/shared/reducers/authentication'; +import PasswordStrengthBar from 'app/shared/layout/password/password-strength-bar'; +import { savePassword, reset } from './password.reducer'; + +export const PasswordPage = () => { + const [password, setPassword] = useState(''); + const dispatch = useAppDispatch(); + + useEffect(() => { + dispatch(reset()); + dispatch(getSession()); + return () => { + dispatch(reset()); + }; + }, []); + + const handleValidSubmit = ({ currentPassword, newPassword }) => { + dispatch(savePassword({ currentPassword, newPassword })); + }; + + const updatePassword = event => setPassword(event.target.value); + + const account = useAppSelector(state => state.authentication.account); + const successMessage = useAppSelector(state => state.password.successMessage); + const errorMessage = useAppSelector(state => state.password.errorMessage); + + useEffect(() => { + if (successMessage) { + toast.success(successMessage); + } else if (errorMessage) { + toast.error(errorMessage); + } + dispatch(reset()); + }, [successMessage, errorMessage]); + + return ( +
+ + +

+ Password for [{account.login}] +

+ + + + + v === password || 'The password and its confirmation do not match!', + }} + data-cy="confirmPassword" + /> + + + +
+
+ ); +}; + +export default 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..d88d78c --- /dev/null +++ b/src/main/webapp/app/modules/account/register/register.reducer.spec.ts @@ -0,0 +1,93 @@ +import axios from 'axios'; +import sinon from 'sinon'; +import { configureStore } from '@reduxjs/toolkit'; + +import register, { handleRegister, reset } from './register.reducer'; + +describe('Creating account tests', () => { + const initialState = { + loading: false, + registrationSuccess: false, + registrationFailure: false, + errorMessage: null, + successMessage: null, + }; + + it('should return the initial state', () => { + expect(register(undefined, { type: '' })).toEqual({ + ...initialState, + }); + }); + + it('should detect a request', () => { + expect(register(undefined, { type: handleRegister.pending.type })).toEqual({ + ...initialState, + loading: true, + }); + }); + + it('should handle RESET', () => { + expect( + register({ loading: true, registrationSuccess: true, registrationFailure: true, errorMessage: '', successMessage: '' }, reset()), + ).toEqual({ + ...initialState, + }); + }); + + it('should handle CREATE_ACCOUNT success', () => { + expect( + register(undefined, { + type: handleRegister.fulfilled.type, + payload: 'fake payload', + }), + ).toEqual({ + ...initialState, + registrationSuccess: true, + successMessage: 'Registration saved! Please check your email for confirmation.', + }); + }); + + it('should handle CREATE_ACCOUNT failure', () => { + const error = { message: 'fake error' }; + expect( + register(undefined, { + type: handleRegister.rejected.type, + error, + }), + ).toEqual({ + ...initialState, + registrationFailure: true, + errorMessage: error.message, + }); + }); + + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + const getState = jest.fn(); + const dispatch = jest.fn(); + const extra = {}; + beforeEach(() => { + store = configureStore({ + reducer: (state = [], action) => [...state, action], + }); + axios.post = sinon.stub().returns(Promise.resolve(resolvedObject)); + }); + + it('dispatches CREATE_ACCOUNT_PENDING and CREATE_ACCOUNT_FULFILLED actions', async () => { + const arg = { login: '', email: '', password: '' }; + + const result = await handleRegister(arg)(dispatch, getState, extra); + + const pendingAction = dispatch.mock.calls[0][0]; + expect(pendingAction.meta.requestStatus).toBe('pending'); + expect(handleRegister.fulfilled.match(result)).toBe(true); + expect(result.payload).toBe(resolvedObject); + }); + it('dispatches RESET actions', async () => { + await store.dispatch(reset()); + expect(store.getState()).toEqual([expect.any(Object), expect.objectContaining(reset())]); + }); + }); +}); 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..1e3f2ec --- /dev/null +++ b/src/main/webapp/app/modules/account/register/register.reducer.ts @@ -0,0 +1,53 @@ +import axios from 'axios'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; + +import { serializeAxiosError } from 'app/shared/reducers/reducer.utils'; + +const initialState = { + loading: false, + registrationSuccess: false, + registrationFailure: false, + errorMessage: null, + successMessage: null, +}; + +export type RegisterState = Readonly; + +// Actions + +export const handleRegister = createAsyncThunk( + 'register/create_account', + async (data: { login: string; email: string; password: string; langKey?: string }) => axios.post('api/register', data), + { serializeError: serializeAxiosError }, +); + +export const RegisterSlice = createSlice({ + name: 'register', + initialState: initialState as RegisterState, + reducers: { + reset() { + return initialState; + }, + }, + extraReducers(builder) { + builder + .addCase(handleRegister.pending, state => { + state.loading = true; + }) + .addCase(handleRegister.rejected, (state, action) => ({ + ...initialState, + registrationFailure: true, + errorMessage: action.error.message, + })) + .addCase(handleRegister.fulfilled, () => ({ + ...initialState, + registrationSuccess: true, + successMessage: 'Registration saved! Please check your email for confirmation.', + })); + }, +}); + +export const { reset } = RegisterSlice.actions; + +// Reducer +export default RegisterSlice.reducer; 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..e208152 --- /dev/null +++ b/src/main/webapp/app/modules/account/register/register.tsx @@ -0,0 +1,125 @@ +import React, { useState, useEffect } from 'react'; +import { ValidatedField, ValidatedForm, isEmail } from 'react-jhipster'; +import { Row, Col, Alert, Button } from 'reactstrap'; +import { toast } from 'react-toastify'; +import { Link } from 'react-router-dom'; + +import PasswordStrengthBar from 'app/shared/layout/password/password-strength-bar'; +import { useAppDispatch, useAppSelector } from 'app/config/store'; +import { handleRegister, reset } from './register.reducer'; + +export const RegisterPage = () => { + const [password, setPassword] = useState(''); + const dispatch = useAppDispatch(); + + useEffect( + () => () => { + dispatch(reset()); + }, + [], + ); + + const handleValidSubmit = ({ username, email, firstPassword }) => { + dispatch(handleRegister({ login: username, email, password: firstPassword, langKey: 'en' })); + }; + + const updatePassword = event => setPassword(event.target.value); + + const successMessage = useAppSelector(state => state.register.successMessage); + + useEffect(() => { + if (successMessage) { + toast.success(successMessage); + } + }, [successMessage]); + + return ( +
+ + +

+ Registration +

+ +
+ + + + + isEmail(v) || 'Your email is invalid.', + }} + data-cy="email" + /> + + + v === password || 'The password and its confirmation do not match!', + }} + data-cy="secondPassword" + /> + + +

 

+ + If you want to + + sign in + + + , you can try the default accounts: +
- Administrator (login="admin" and password="admin")
- User (login="user" and + password="user"). +
+
+ +
+
+ ); +}; + +export default 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..71d94f0 --- /dev/null +++ b/src/main/webapp/app/modules/account/settings/settings.reducer.spec.ts @@ -0,0 +1,90 @@ +import { configureStore } from '@reduxjs/toolkit'; +import axios from 'axios'; +import sinon from 'sinon'; + +import account, { updateAccount, reset } from './settings.reducer'; + +describe('Settings reducer tests', () => { + describe('Common tests', () => { + it('should return the initial state', () => { + const toTest = account(undefined, { type: '' }); + expect(toTest).toMatchObject({ + loading: false, + errorMessage: null, + updateSuccess: false, + updateFailure: false, + }); + }); + }); + + describe('Settings update', () => { + it('should detect a request', () => { + const toTest = account(undefined, { type: updateAccount.pending.type }); + expect(toTest).toMatchObject({ + updateSuccess: false, + updateFailure: false, + loading: true, + }); + }); + it('should detect a success', () => { + const toTest = account(undefined, { type: updateAccount.fulfilled.type }); + expect(toTest).toMatchObject({ + updateSuccess: true, + updateFailure: false, + loading: false, + }); + }); + it('should detect a failure', () => { + const toTest = account(undefined, { type: updateAccount.rejected.type }); + expect(toTest).toMatchObject({ + updateSuccess: false, + updateFailure: true, + loading: false, + }); + }); + + it('should reset the state', () => { + const initialState = { + loading: false, + errorMessage: null, + successMessage: null, + updateSuccess: false, + updateFailure: false, + }; + expect(account({ ...initialState, loading: true }, reset())).toEqual({ + ...initialState, + }); + }); + }); + + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + const getState = jest.fn(); + const dispatch = jest.fn(); + const extra = {}; + beforeEach(() => { + store = configureStore({ + reducer: (state = [], action) => [...state, action], + }); + 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 arg = ''; + + const result = await updateAccount(arg)(dispatch, getState, extra); + + const pendingAction = dispatch.mock.calls[0][0]; + expect(pendingAction.meta.requestStatus).toBe('pending'); + expect(updateAccount.fulfilled.match(result)).toBe(true); + expect(result.payload).toBe(resolvedObject); + }); + it('dispatches RESET actions', async () => { + await store.dispatch(reset()); + expect(store.getState()).toEqual([expect.any(Object), expect.objectContaining(reset())]); + }); + }); +}); 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..d194529 --- /dev/null +++ b/src/main/webapp/app/modules/account/settings/settings.reducer.ts @@ -0,0 +1,63 @@ +import axios from 'axios'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; + +import { getSession } from 'app/shared/reducers/authentication'; +import { AppThunk } from 'app/config/store'; +import { serializeAxiosError } from 'app/shared/reducers/reducer.utils'; + +const initialState = { + loading: false, + errorMessage: null, + successMessage: null, + updateSuccess: false, + updateFailure: false, +}; + +export type SettingsState = Readonly; + +// Actions +const apiUrl = 'api/account'; + +export const saveAccountSettings: (account: any) => AppThunk = account => async dispatch => { + await dispatch(updateAccount(account)); + + dispatch(getSession()); +}; + +export const updateAccount = createAsyncThunk('settings/update_account', async (account: any) => axios.post(apiUrl, account), { + serializeError: serializeAxiosError, +}); + +export const SettingsSlice = createSlice({ + name: 'settings', + initialState: initialState as SettingsState, + reducers: { + reset() { + return initialState; + }, + }, + extraReducers(builder) { + builder + .addCase(updateAccount.pending, state => { + state.loading = true; + state.errorMessage = null; + state.updateSuccess = false; + }) + .addCase(updateAccount.rejected, state => { + state.loading = false; + state.updateSuccess = false; + state.updateFailure = true; + }) + .addCase(updateAccount.fulfilled, state => { + state.loading = false; + state.updateSuccess = true; + state.updateFailure = false; + state.successMessage = 'Settings saved!'; + }); + }, +}); + +export const { reset } = SettingsSlice.actions; + +// Reducer +export default SettingsSlice.reducer; 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..7bac991 --- /dev/null +++ b/src/main/webapp/app/modules/account/settings/settings.tsx @@ -0,0 +1,92 @@ +import React, { useEffect } from 'react'; +import { Button, Col, Row } from 'reactstrap'; +import { ValidatedField, ValidatedForm, isEmail } from 'react-jhipster'; +import { toast } from 'react-toastify'; + +import { useAppDispatch, useAppSelector } from 'app/config/store'; +import { getSession } from 'app/shared/reducers/authentication'; +import { saveAccountSettings, reset } from './settings.reducer'; + +export const SettingsPage = () => { + const dispatch = useAppDispatch(); + const account = useAppSelector(state => state.authentication.account); + const successMessage = useAppSelector(state => state.settings.successMessage); + + useEffect(() => { + dispatch(getSession()); + return () => { + dispatch(reset()); + }; + }, []); + + useEffect(() => { + if (successMessage) { + toast.success(successMessage); + } + }, [successMessage]); + + const handleValidSubmit = values => { + dispatch( + saveAccountSettings({ + ...account, + ...values, + }), + ); + }; + + return ( +
+ + +

+ User settings for [{account.login}] +

+ + + + isEmail(v) || 'Your email is invalid.', + }} + data-cy="email" + /> + + + +
+
+ ); +}; + +export default 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..24a4627 --- /dev/null +++ b/src/main/webapp/app/modules/administration/administration.reducer.spec.ts @@ -0,0 +1,232 @@ +import axios from 'axios'; +import sinon from 'sinon'; +import { configureStore } from '@reduxjs/toolkit'; + +import administration, { + getSystemHealth, + getSystemMetrics, + getSystemThreadDump, + getLoggers, + getConfigurations, + getEnv, + setLoggers, +} from './administration.reducer'; + +describe('Administration reducer tests', () => { + function isEmpty(element): boolean { + if (element instanceof Array) { + return element.length === 0; + } + 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, error?) { + types.forEach(e => { + testFunction(administration(undefined, { type: e, payload, error })); + }); + } + + describe('Common', () => { + it('should return the initial state', () => { + testInitialState(administration(undefined, { type: '' })); + }); + }); + + describe('Requests', () => { + it('should set state to loading', () => { + testMultipleTypes( + [ + getLoggers.pending.type, + getSystemHealth.pending.type, + getSystemMetrics.pending.type, + getSystemThreadDump.pending.type, + getConfigurations.pending.type, + getEnv.pending.type, + ], + {}, + state => { + expect(state).toMatchObject({ + errorMessage: null, + loading: true, + }); + }, + ); + }); + }); + + describe('Failures', () => { + it('should set state to failed and put an error message in errorMessage', () => { + testMultipleTypes( + [ + getLoggers.rejected.type, + getSystemHealth.rejected.type, + getSystemMetrics.rejected.type, + getSystemThreadDump.rejected.type, + getConfigurations.rejected.type, + getEnv.rejected.type, + ], + 'something happened', + state => { + expect(state).toMatchObject({ + loading: false, + errorMessage: 'error', + }); + }, + { + message: 'error', + }, + ); + }); + }); + + 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: getLoggers.fulfilled.type, 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: getSystemHealth.fulfilled.type, 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: getSystemMetrics.fulfilled.type, 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: getSystemThreadDump.fulfilled.type, 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: getConfigurations.fulfilled.type, 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: getEnv.fulfilled.type, payload }); + + expect(toTest).toMatchObject({ + loading: false, + configuration: { + configProps: {}, + env: payload.data, + }, + }); + }); + }); + describe('Actions', () => { + let store; + + const resolvedObject = { value: 'whatever' }; + const getState = jest.fn(); + const dispatch = jest.fn(); + const extra = {}; + beforeEach(() => { + store = configureStore({ + reducer: (state = [], action) => [...state, action], + }); + 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 result = await getSystemHealth()(dispatch, getState, extra); + + const pendingAction = dispatch.mock.calls[0][0]; + expect(pendingAction.meta.requestStatus).toBe('pending'); + expect(getSystemHealth.fulfilled.match(result)).toBe(true); + expect(result.payload).toBe(resolvedObject); + }); + it('dispatches FETCH_METRICS_PENDING and FETCH_METRICS_FULFILLED actions', async () => { + const result = await getSystemMetrics()(dispatch, getState, extra); + + const pendingAction = dispatch.mock.calls[0][0]; + expect(pendingAction.meta.requestStatus).toBe('pending'); + expect(getSystemMetrics.fulfilled.match(result)).toBe(true); + }); + it('dispatches FETCH_THREAD_DUMP_PENDING and FETCH_THREAD_DUMP_FULFILLED actions', async () => { + const result = await getSystemThreadDump()(dispatch, getState, extra); + + const pendingAction = dispatch.mock.calls[0][0]; + expect(pendingAction.meta.requestStatus).toBe('pending'); + expect(getSystemThreadDump.fulfilled.match(result)).toBe(true); + }); + it('dispatches FETCH_LOGS_PENDING and FETCH_LOGS_FULFILLED actions', async () => { + const result = await getLoggers()(dispatch, getState, extra); + + const pendingAction = dispatch.mock.calls[0][0]; + expect(pendingAction.meta.requestStatus).toBe('pending'); + expect(getLoggers.fulfilled.match(result)).toBe(true); + }); + it('dispatches FETCH_LOGS_CHANGE_LEVEL_PENDING and FETCH_LOGS_CHANGE_LEVEL_FULFILLED actions', async () => { + const result = await setLoggers({ name: 'ROOT', configuredLevel: 'DEBUG' })(dispatch, getState, extra); + + const pendingAction = dispatch.mock.calls[0][0]; + expect(pendingAction.meta.requestStatus).toBe('pending'); + expect(setLoggers.fulfilled.match(result)).toBe(true); + }); + it('dispatches FETCH_CONFIGURATIONS_PENDING and FETCH_CONFIGURATIONS_FULFILLED actions', async () => { + const result = await getConfigurations()(dispatch, getState, extra); + + const pendingAction = dispatch.mock.calls[0][0]; + expect(pendingAction.meta.requestStatus).toBe('pending'); + expect(getConfigurations.fulfilled.match(result)).toBe(true); + }); + it('dispatches FETCH_ENV_PENDING and FETCH_ENV_FULFILLED actions', async () => { + const result = await getEnv()(dispatch, getState, extra); + + const pendingAction = dispatch.mock.calls[0][0]; + expect(pendingAction.meta.requestStatus).toBe('pending'); + expect(getEnv.fulfilled.match(result)).toBe(true); + }); + }); +}); 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..c2442ba --- /dev/null +++ b/src/main/webapp/app/modules/administration/administration.reducer.ts @@ -0,0 +1,125 @@ +import axios from 'axios'; +import { createAsyncThunk, createSlice, isPending, isRejected } from '@reduxjs/toolkit'; + +import { serializeAxiosError } from 'app/shared/reducers/reducer.utils'; +import { AppThunk } from 'app/config/store'; + +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; + +// Actions + +export const getSystemHealth = createAsyncThunk('administration/fetch_health', async () => axios.get('management/health'), { + serializeError: serializeAxiosError, +}); + +export const getSystemMetrics = createAsyncThunk('administration/fetch_metrics', async () => axios.get('management/jhimetrics'), { + serializeError: serializeAxiosError, +}); + +export const getSystemThreadDump = createAsyncThunk( + 'administration/fetch_thread_dump', + async () => axios.get('management/threaddump'), + { + serializeError: serializeAxiosError, + }, +); + +export const getLoggers = createAsyncThunk('administration/fetch_logs', async () => axios.get('management/loggers'), { + serializeError: serializeAxiosError, +}); + +export const setLoggers = createAsyncThunk( + 'administration/fetch_logs_change_level', + async ({ name, configuredLevel }: any) => axios.post(`management/loggers/${name}`, { configuredLevel }), + { + serializeError: serializeAxiosError, + }, +); + +export const changeLogLevel: (name, configuredLevel) => AppThunk = (name, configuredLevel) => async dispatch => { + await dispatch(setLoggers({ name, configuredLevel })); + dispatch(getLoggers()); +}; + +export const getConfigurations = createAsyncThunk( + 'administration/fetch_configurations', + async () => axios.get('management/configprops'), + { + serializeError: serializeAxiosError, + }, +); + +export const getEnv = createAsyncThunk('administration/fetch_env', async () => axios.get('management/env'), { + serializeError: serializeAxiosError, +}); + +export const AdministrationSlice = createSlice({ + name: 'administration', + initialState: initialState as AdministrationState, + reducers: {}, + extraReducers(builder) { + builder + .addCase(getSystemHealth.fulfilled, (state, action) => { + state.loading = false; + state.health = action.payload.data; + }) + .addCase(getSystemMetrics.fulfilled, (state, action) => { + state.loading = false; + state.metrics = action.payload.data; + }) + .addCase(getSystemThreadDump.fulfilled, (state, action) => { + state.loading = false; + state.threadDump = action.payload.data; + }) + .addCase(getLoggers.fulfilled, (state, action) => { + state.loading = false; + state.logs = { + loggers: action.payload.data.loggers, + }; + }) + .addCase(getConfigurations.fulfilled, (state, action) => { + state.loading = false; + state.configuration = { + ...state.configuration, + configProps: action.payload.data, + }; + }) + .addCase(getEnv.fulfilled, (state, action) => { + state.loading = false; + state.configuration = { + ...state.configuration, + env: action.payload.data, + }; + }) + .addMatcher(isPending(getSystemHealth, getSystemMetrics, getSystemThreadDump, getLoggers, getConfigurations, getEnv), state => { + state.errorMessage = null; + state.loading = true; + }) + .addMatcher( + isRejected(getSystemHealth, getSystemMetrics, getSystemThreadDump, getLoggers, getConfigurations, getEnv), + (state, action) => { + state.errorMessage = action.error.message; + state.loading = false; + }, + ); + }, +}); + +// Reducer +export default AdministrationSlice.reducer; 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..90f78d1 --- /dev/null +++ b/src/main/webapp/app/modules/administration/configuration/configuration.tsx @@ -0,0 +1,108 @@ +import React, { useState, useEffect } from 'react'; +import { Table, Input, Row, Col, Badge } from 'reactstrap'; + +import { getConfigurations, getEnv } from '../administration.reducer'; +import { useAppDispatch, useAppSelector } from 'app/config/store'; + +export const ConfigurationPage = () => { + const [filter, setFilter] = useState(''); + const [reversePrefix, setReversePrefix] = useState(false); + const [reverseProperties, setReverseProperties] = useState(false); + const dispatch = useAppDispatch(); + + const configuration = useAppSelector(state => state.administration.configuration); + + useEffect(() => { + dispatch(getConfigurations()); + dispatch(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 configProps = configuration?.configProps ?? {}; + + const env = configuration?.env ?? {}; + + return ( +
+

+ Configuration +

+ Filter (by prefix) + + + + + + + + + + {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} +
+ ); +}; + +export default 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 = () => ( +
+