diff --git a/.forgejo/workflows/ci-settings.xml b/.forgejo/workflows/ci-settings.xml new file mode 100644 index 0000000..c206c4b --- /dev/null +++ b/.forgejo/workflows/ci-settings.xml @@ -0,0 +1,37 @@ + + + + + + nexus + * + http://repo.oguerreiro.com/repository/maven-public/ + + + + + + nexus + default + default#123! + + + maven-releases + default + default#123! + + + maven-snapshots + default + default#123! + + + diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index bf4c7d3..7b8d83e 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -14,7 +14,7 @@ jobs: - name: Install system dependencies run: | apt-get update - apt-get install -y nodejs npm openjdk-17-jdk maven git + apt-get install -y nodejs npm openjdk-17-jdk maven git jq export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which javac)))) echo "JAVA_HOME=$JAVA_HOME" >> $GITHUB_ENV @@ -22,6 +22,7 @@ jobs: run: | java -version mvn -v + mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout node -v npm -v @@ -52,6 +53,106 @@ jobs: git commit -am "Release ${{ steps.version.outputs.release_version }}" git push + # Create the release notes (closed issues) + # Must be here, before the new tag, to get closed issued from the LAST TAG to NOW + - name: Generate release notes from closed issues + id: generate_notes + run: | + REPO_OWNER="root" + REPO_NAME="resilient" + TOKEN="${{ secrets.FORGEJO_TOKEN }}" + VERSION="${{ steps.version.outputs.release_version }}" + API_BASE="https://git.oguerreiro.com/api/v1" + + # Step 1. Get the latest tag + echo "Step 1 : Fetching latest release tag..." + LATEST_TAG=$(curl -H "Authorization: token $TOKEN" \ + "$API_BASE/repos/$REPO_OWNER/$REPO_NAME/tags" \ + | jq -r '.[0].name') + + if [[ -z "$LATEST_TAG" || "$LATEST_TAG" == "null" ]]; then + # Doesn't have tag. Its the first release + echo "No previous tags found. Assuming first release." + CLOSED_ISSUES="" + else + # Tag found + echo "Latest tag is: $LATEST_TAG" + + # Step 2: Get the tag ref object + echo "Step 2 : Fetch tag ref object from tag = $LATEST_TAG" + TAG_REF=$(curl -H "Authorization: token $TOKEN" \ + "$API_BASE/repos/$REPO_OWNER/$REPO_NAME/git/refs/tags/$LATEST_TAG") + echo "... TAG_REF is: $TAG_REF" + + # Extract the first .object.sha (if it's an array) or directly if it's an object + if echo "$TAG_REF" | jq -e 'type == "array"' >/dev/null; then + echo "... is an array" + TAG_OBJECT_SHA=$(echo "$TAG_REF" | jq -r '.[0].object.sha') + TAG_OBJECT_TYPE=$(echo "$TAG_REF" | jq -r '.[0].object.type') + else + echo "... is an object" + TAG_OBJECT_SHA=$(echo "$TAG_REF" | jq -r '.object.sha') + TAG_OBJECT_TYPE=$(echo "$TAG_REF" | jq -r '.object.type') + fi + + echo "... TAG SHA is (1): $TAG_OBJECT_SHA" + echo "... TAG TYPE is (1): $TAG_OBJECT_TYPE" + + # Resolve annotated tag to commit SHA + while [ "$TAG_OBJECT_TYPE" != "commit" ]; do + echo "..." + echo "... TAG OBJECT is not of type commit. Try : $API_BASE/repos/$REPO_OWNER/$REPO_NAME/git/$TAG_OBJECT_TYPE/$TAG_OBJECT_SHA" + TAG_OBJECT=$(curl -H "Authorization: token $TOKEN" \ + "$API_BASE/repos/$REPO_OWNER/$REPO_NAME/git/tags/$TAG_OBJECT_SHA") + + if echo "$TAG_OBJECT" | jq -e 'type == "array"' >/dev/null; then + echo "... ... is an array" + TAG_OBJECT_SHA=$(echo "$v" | jq -r '.[0].object.sha') + TAG_OBJECT_TYPE=$(echo "$TAG_OBJECT" | jq -r '.[0].object.type') + else + echo "... ... is an object" + TAG_OBJECT_SHA=$(echo "$TAG_OBJECT" | jq -r '.object.sha') + TAG_OBJECT_TYPE=$(echo "$TAG_OBJECT" | jq -r '.object.type') + fi + + echo "... TAG_OBJECT_SHA is (2): $TAG_OBJECT_SHA" + echo "... TAG_OBJECT_TYPE is (2): $TAG_OBJECT_TYPE" + done + + echo "... commit found with SHA $TAG_OBJECT_SHA" + COMMIT_SHA=$TAG_OBJECT_SHA + + # Step 3: Get the commit date + echo "Step 3 : Get the commit date from commit = $COMMIT_SHA" + COMMIT_DATE=$(curl -H "Authorization: token $TOKEN" \ + "$API_BASE/repos/$REPO_OWNER/$REPO_NAME/git/commits/$COMMIT_SHA" \ + | jq -r '.commit.committer.date') + echo "... Fetching closed issues since $COMMIT_DATE..." + + CLOSED_ISSUES_JSON=$(curl -H "Authorization: token $TOKEN" \ + "$API_BASE/repos/$REPO_OWNER/$REPO_NAME/issues?state=closed&since=$COMMIT_DATE") + + if [[ -z "$CLOSED_ISSUES_JSON" ]]; then + echo "Warning: No closed issues or invalid response." + CLOSED_ISSUES="" + else + CLOSED_ISSUES=$(echo "$CLOSED_ISSUES_JSON" | jq -r '.[] | "- [#\(.number)](\(.html_url)) \(.title)"') + fi + fi + + # Build release notes markdown + { + echo "## Release v$VERSION" + echo + if [[ -n "$CLOSED_ISSUES" ]]; then + echo "### Closed Issues" + echo "$CLOSED_ISSUES" + else + echo "No issues closed since last release." + fi + } > release-notes.md + cat release-notes.md + - name: Tag release run: | git tag -a v${{ steps.version.outputs.release_version }} -m "Release ${{ steps.version.outputs.release_version }}" @@ -61,45 +162,42 @@ jobs: # Install @Angular dependencies - name: Install frontend dependencies + if: false # DISABLED for testing. To check if backend build is also doing frontend. run: | cd src/main/webapp npm ci - # Setup node_modules cache, for better performance - - name: Cache node_modules - uses: actions/cache@v3 - with: - path: | - **/node_modules - key: node-modules-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - node-modules- - # Build the frontend - name: Build frontend + if: false # DISABLED for testing. To check if backend build is also doing frontend. run: | cd src/main/webapp npm run build - - # Setup Maven cache, for better performance - - name: Cache Maven dependencies - uses: actions/cache@v3 - with: - path: ~/.m2/repository - key: maven-${{ runner.os }}-${{ hashFiles('**/pom.xml') }} - restore-keys: | - maven-${{ runner.os }}- - - # Build the backend (JAR) + + # Build the backend (JAR). NOTE: the -Dmaven.download.parallel=false will force MAVEN to have a single connection for downloads, without this I can get "Connection reset" - name: Build backend (Spring Boot) run: | - mvn clean package -Pprod -DskipTests - + echo "DEBUG Outputs ************************* " + echo "Container hostname: $(hostname)" + echo "Runner working dir: $PWD" + echo "Runner user: $(whoami)" + echo "Listing contents of ~/.m2/repository:" + ls -lhR ~/.m2/repository | head -n 100 || echo "No .m2/repository found" + echo "Disk usage:" + du -sh ~/.m2/repository || echo "No .m2 directory found" + echo " " + echo "BUILD Outputs ************************* " + mvn clean package -Pprod -DskipTests -Dmaven.download.parallel=false -Dmaven.repo.local=/root/.m2/repository -s .forgejo/workflows/ci-settings.xml + container: + image: maven:3.9-eclipse-temurin-17 + volumes: + - docker-forgejo_maven-cache:/root/.m2 + # Log output - name: List output files run: | ls -lh target/*.jar || true - + # save artifacts - name: Upload backend JAR uses: actions/upload-artifact@v3 @@ -107,6 +205,38 @@ jobs: name: app-backend path: target/resilient*.jar + # Create the release + # NOTE: added system dependency install "jq" + - name: Create release and upload JAR + run: | + VERSION=${{ steps.version.outputs.release_version }} + API="https://git.oguerreiro.com/api/v1" + REPO="root/resilient" + + # Create release + RESPONSE=$(curl -X POST "$API/repos/$REPO/releases" \ + -H "Authorization: token ${{ secrets.FORGEJO_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d "{ + \"tag_name\": \"v$VERSION\", + \"target_commitish\": \"master\", + \"name\": \"Release v$VERSION\", + \"body\": $(jq -Rs < release-notes.md) + }") + + RELEASE_ID=$(echo "$RESPONSE" | jq -r '.id') + echo "Created release ID: $RELEASE_ID" + + # Upload artifact + # NOTE: For safety, the artifact to upload is expected to be the EXACT $VERSION of this branch. If not, something is wrong. + JAR_FILE=$(ls target/resilient-$VERSION.jar | head -n 1) + echo "Attaching file : $JAR_FILE" + curl -X POST "$API/repos/$REPO/releases/$RELEASE_ID/assets?name=resilient-$VERSION.jar" \ + -H "Authorization: token ${{ secrets.FORGEJO_TOKEN }}" \ + -H "Content-Type: application/java-archive" \ + --data-binary @"$JAR_FILE" + + # ################################################################## # ## THIS IS A RELEASE - Change master version to next SNAPSHOT ## # ## #4. Checkout master ## diff --git a/pom.xml b/pom.xml index 7b03f23..57fcf51 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.oguerreiro.resilient resilient - 1.0.3-SNAPSHOT + 1.0.8-SNAPSHOT jar Resilient Description for Resilient diff --git a/src/main/java/com/oguerreiro/resilient/config/SecurityConfiguration.java b/src/main/java/com/oguerreiro/resilient/config/SecurityConfiguration.java index ce677fb..eb2f037 100644 --- a/src/main/java/com/oguerreiro/resilient/config/SecurityConfiguration.java +++ b/src/main/java/com/oguerreiro/resilient/config/SecurityConfiguration.java @@ -219,6 +219,7 @@ public class SecurityConfiguration { .requestMatchers(mvc.pattern("/api/account/reset-password/init")).permitAll() .requestMatchers(mvc.pattern("/api/account/reset-password/finish")).permitAll() .requestMatchers(mvc.pattern("/api/account/saml2-endpoint")).permitAll() + .requestMatchers(mvc.pattern("/api/account/has-basic-auth")).permitAll() .requestMatchers(mvc.pattern("/api/home/page")).permitAll() .requestMatchers(mvc.pattern("/api/admin/**")).hasAuthority(AuthoritiesConstants.ADMIN) .requestMatchers(mvc.pattern("/api/**")).authenticated() diff --git a/src/main/java/com/oguerreiro/resilient/repository/OrganizationRepository.java b/src/main/java/com/oguerreiro/resilient/repository/OrganizationRepository.java index 8ba65ab..5aef3a7 100644 --- a/src/main/java/com/oguerreiro/resilient/repository/OrganizationRepository.java +++ b/src/main/java/com/oguerreiro/resilient/repository/OrganizationRepository.java @@ -5,6 +5,7 @@ import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @@ -38,11 +39,13 @@ public interface OrganizationRepository extends ResilientJpaRepository findOneByUser(User user); @Query("select organization from Organization organization where organization.outputInventory = true and organization.organizationType.nature = com.oguerreiro.resilient.domain.enumeration.OrganizationNature.ORGANIZATION order by (CASE WHEN organization.parent IS NULL THEN 0 ELSE 1 END), (CASE WHEN organization.sort IS NULL THEN 1 ELSE 0 END), organization.sort asc") List findAllOrganizationForOutput(); + @EntityGraph(attributePaths = "parent") Optional findOneByCode(String code); @Query(""" diff --git a/src/main/java/com/oguerreiro/resilient/security/basic/ResilientBasicProperties.java b/src/main/java/com/oguerreiro/resilient/security/basic/ResilientBasicProperties.java new file mode 100644 index 0000000..d279cb7 --- /dev/null +++ b/src/main/java/com/oguerreiro/resilient/security/basic/ResilientBasicProperties.java @@ -0,0 +1,22 @@ +package com.oguerreiro.resilient.security.basic; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "resilient.security.basic") +public class ResilientBasicProperties { + + private boolean enabled; + + // Root-level Getters/Setters + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + +} diff --git a/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationHandler.java b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationHandler.java index f072998..1da77e8 100644 --- a/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationHandler.java +++ b/src/main/java/com/oguerreiro/resilient/security/saml2/Saml2AuthenticationHandler.java @@ -155,8 +155,9 @@ public class Saml2AuthenticationHandler implements AuthenticationSuccessHandler, } // Create a ResilientUserDetails and replace Principal + Organization parentOrganization = userOrganization.getParent(); ResilientUserDetails userdetails = new ResilientUserDetails(username, "MOCK-PWD", authorities, securityGroup, - userOrganization, "pt-PT"); + userOrganization.getParent(), "pt-PT"); Saml2Authentication newAuthentication = new Saml2Authentication(userdetails, samlXMLResponse, userdetails.getAuthorities()); diff --git a/src/main/java/com/oguerreiro/resilient/service/DashboardComponentService.java b/src/main/java/com/oguerreiro/resilient/service/DashboardComponentService.java index 3e812a2..8eeb794 100644 --- a/src/main/java/com/oguerreiro/resilient/service/DashboardComponentService.java +++ b/src/main/java/com/oguerreiro/resilient/service/DashboardComponentService.java @@ -33,6 +33,8 @@ import com.oguerreiro.resilient.domain.enumeration.DashboardComponentView; import com.oguerreiro.resilient.repository.DashboardComponentRepository; import com.oguerreiro.resilient.repository.OrganizationRepository; import com.oguerreiro.resilient.service.dto.DashboardComponentDTO; +import com.oguerreiro.resilient.service.dto.DashboardComponentDetailValueDTO; +import com.oguerreiro.resilient.service.mapper.DashboardComponentDetailValueMapper; import com.oguerreiro.resilient.service.mapper.DashboardComponentMapper; /** @@ -45,13 +47,25 @@ public class DashboardComponentService private OrganizationRepository organizationRepository; private DashboardComponentMapper dashboardComponentMapper; + private final DashboardComponentDetailValueMapper dashboardComponentDetailValueMapper; public DashboardComponentService(DashboardComponentRepository dashboardComponentRepository, - DashboardComponentMapper dashboardComponentMapper, OrganizationRepository organizationRepository) { + DashboardComponentMapper dashboardComponentMapper, OrganizationRepository organizationRepository, + DashboardComponentDetailValueMapper dashboardComponentDetailValueMapper) { super(DashboardComponent.class, dashboardComponentRepository, dashboardComponentMapper); this.organizationRepository = organizationRepository; this.dashboardComponentMapper = dashboardComponentMapper; + this.dashboardComponentDetailValueMapper = dashboardComponentDetailValueMapper; + } + + public Map> buildDashboardComponentViewDTO(Long organizationId, + Long dashboardComponentId, Long periodVersionId) { + + Map> mapValues = buildDashboardComponentView(organizationId, + dashboardComponentId, periodVersionId); + + return this.dashboardComponentDetailValueMapper.mapToDto(mapValues); } /* diff --git a/src/main/java/com/oguerreiro/resilient/service/UserService.java b/src/main/java/com/oguerreiro/resilient/service/UserService.java index c2f26b8..ad31be1 100644 --- a/src/main/java/com/oguerreiro/resilient/service/UserService.java +++ b/src/main/java/com/oguerreiro/resilient/service/UserService.java @@ -151,6 +151,7 @@ public class UserService { } public User createUser(AdminUserDTO userDTO) { + User user = new User(); user.setLogin(userDTO.getLogin().toLowerCase()); user.setFirstName(userDTO.getFirstName()); @@ -169,6 +170,8 @@ public class UserService { user.setResetKey(RandomUtil.generateResetKey()); user.setResetDate(Instant.now()); user.setActivated(true); + user.setAllowSamlAuthentication(userDTO.isAllowSamlAuthentication()); + user.setAllowUserPwdAuthentication(userDTO.isAllowUserPwdAuthentication()); if (userDTO.getAuthorities() != null) { Set authorities = userDTO.getAuthorities().stream().map(authorityRepository::findById).filter( Optional::isPresent).map(Optional::get).collect(Collectors.toSet()); diff --git a/src/main/java/com/oguerreiro/resilient/web/rest/AccountResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/AccountResource.java index d4eb475..3aab272 100644 --- a/src/main/java/com/oguerreiro/resilient/web/rest/AccountResource.java +++ b/src/main/java/com/oguerreiro/resilient/web/rest/AccountResource.java @@ -31,6 +31,7 @@ import com.oguerreiro.resilient.domain.User; import com.oguerreiro.resilient.repository.PersistentTokenRepository; import com.oguerreiro.resilient.repository.UserRepository; import com.oguerreiro.resilient.security.SecurityUtils; +import com.oguerreiro.resilient.security.basic.ResilientBasicProperties; import com.oguerreiro.resilient.security.custom.ResilientUserDetails; import com.oguerreiro.resilient.service.MailService; import com.oguerreiro.resilient.service.UserService; @@ -71,14 +72,18 @@ public class AccountResource { private final RelyingPartyRegistrationRepository registrationRepository; + private ResilientBasicProperties resilientBasicProperties; + public AccountResource(UserRepository userRepository, UserService userService, MailService mailService, PersistentTokenRepository persistentTokenRepository, - @Autowired(required = false) RelyingPartyRegistrationRepository registrationRepository) { + @Autowired(required = false) RelyingPartyRegistrationRepository registrationRepository, + ResilientBasicProperties resilientBasicProperties) { this.userRepository = userRepository; this.userService = userService; this.mailService = mailService; this.persistentTokenRepository = persistentTokenRepository; this.registrationRepository = registrationRepository; + this.resilientBasicProperties = resilientBasicProperties; } /** @@ -192,6 +197,10 @@ public class AccountResource { */ @PostMapping(path = "/account/change-password") public void changePassword(@RequestBody PasswordChangeDTO passwordChangeDto) { + if (!this.isBasicAuthActive()) { + throw new AccountResourceException("Invalid or not allowed request"); + } + if (isPasswordLengthInvalid(passwordChangeDto.getNewPassword())) { throw new InvalidPasswordException(); } @@ -244,6 +253,10 @@ public class AccountResource { */ @PostMapping(path = "/account/reset-password/init") public void requestPasswordReset(@RequestBody String mail) { + if (!this.isBasicAuthActive()) { + throw new AccountResourceException("Invalid or not allowed request"); + } + Optional user = userService.requestPasswordReset(mail); if (user.isPresent()) { mailService.sendPasswordResetMail(user.orElseThrow()); @@ -263,6 +276,10 @@ public class AccountResource { */ @PostMapping(path = "/account/reset-password/finish") public void finishPasswordReset(@RequestBody KeyAndPasswordVM keyAndPassword) { + if (!this.isBasicAuthActive()) { + throw new AccountResourceException("Invalid or not allowed request"); + } + if (isPasswordLengthInvalid(keyAndPassword.getNewPassword())) { throw new InvalidPasswordException(); } @@ -294,6 +311,19 @@ public class AccountResource { return ids.get(0); } + @GetMapping(path = "/account/has-basic-auth") + public Boolean hasBasicAuthentication() { + return this.isBasicAuthActive(); + } + + private boolean isBasicAuthActive() { + if (this.resilientBasicProperties != null && this.resilientBasicProperties.isEnabled()) { + return this.resilientBasicProperties.isEnabled(); + } + + return Boolean.FALSE; + } + 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/oguerreiro/resilient/web/rest/DashboardComponentResource.java b/src/main/java/com/oguerreiro/resilient/web/rest/DashboardComponentResource.java index 1724b5b..2d98ef6 100644 --- a/src/main/java/com/oguerreiro/resilient/web/rest/DashboardComponentResource.java +++ b/src/main/java/com/oguerreiro/resilient/web/rest/DashboardComponentResource.java @@ -15,13 +15,11 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.oguerreiro.resilient.domain.DashboardComponent; -import com.oguerreiro.resilient.domain.DashboardComponentDetailValue; import com.oguerreiro.resilient.domain.enumeration.DashboardComponentView; import com.oguerreiro.resilient.repository.DashboardComponentRepository; import com.oguerreiro.resilient.service.DashboardComponentService; import com.oguerreiro.resilient.service.dto.DashboardComponentDTO; import com.oguerreiro.resilient.service.dto.DashboardComponentDetailValueDTO; -import com.oguerreiro.resilient.service.mapper.DashboardComponentDetailValueMapper; /** * REST controller for managing {@link com.oguerreiro.resilient.domain.UnitType}. @@ -32,14 +30,11 @@ public class DashboardComponentResource extends AbstractResilientResource { private static final String ENTITY_NAME = "dashboardComponent"; - private final DashboardComponentDetailValueMapper dashboardComponentDetailValueMapper; private final DashboardComponentService dashboardComponentService; public DashboardComponentResource(DashboardComponentRepository dashboardComponentRepository, - DashboardComponentService dashboardComponentService, - DashboardComponentDetailValueMapper dashboardComponentDetailValueMapper) { + DashboardComponentService dashboardComponentService) { super(DashboardComponent.class, dashboardComponentRepository, dashboardComponentService); - this.dashboardComponentDetailValueMapper = dashboardComponentDetailValueMapper; this.dashboardComponentService = dashboardComponentService; } @@ -67,10 +62,10 @@ public class DashboardComponentResource Optional dashboardComponentDTO = getDashboardComponentService().findOne( dashboardComponentId); - Map> values = getDashboardComponentService().buildDashboardComponentView( + Map> values = getDashboardComponentService().buildDashboardComponentViewDTO( null, dashboardComponentId, periodVersionId); - return this.dashboardComponentDetailValueMapper.mapToDto(values); + return values; } @GetMapping("/build/{organizationId}/{dashboardComponentId}/{periodVersionId}") @@ -82,10 +77,10 @@ public class DashboardComponentResource Optional dashboardComponentDTO = getDashboardComponentService().findOne( dashboardComponentId); - Map> values = getDashboardComponentService().buildDashboardComponentView( + Map> values = getDashboardComponentService().buildDashboardComponentViewDTO( organizationId, dashboardComponentId, periodVersionId); - return this.dashboardComponentDetailValueMapper.mapToDto(values); + return values; } @GetMapping("/active") diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index 0108b50..ecd98aa 100644 --- a/src/main/resources/config/application-dev.yml +++ b/src/main/resources/config/application-dev.yml @@ -23,6 +23,8 @@ logging: org.opensaml: DEBUG org.springframework.security.saml2: DEBUG org.springframework.security: DEBUG + org.hibernate.proxy: TRACE + org.hibernate.bytecode: TRACE spring: jpa: @@ -144,9 +146,11 @@ resilient: enabled: false port: 8081 mock-idp: - enabled: false + enabled: true path: classpath:mock-idp/idp.js security: + basic: # ADDED to config the formLogin (user+password). This allows for the DISABLE of basic authentication + enabled: true saml2: # ADDED to support SAMLv2 authentication to IDP. # Metadata endpoint ${base-url}/saml2/service-provider-metadata/mock-idp enabled: true @@ -172,9 +176,9 @@ resilient: name: name # the user display name [OPTIONAL] username: urn:mace:dir:attribute-def:mail # the username, typically for authentication. Fallsback to email. [MANDATORY] email: email # the user email [MANDATORY] - organization-code: organization_code # organization unit code [OPTIONAL] - security-group-code: security_group # security group code [OPTIONAL] - role: roles # a single role is expected [OPTIONAL] + organization-code: # organization unit code [OPTIONAL]. Eg. "organization_code" + security-group-code: # security group code [OPTIONAL]. Eg. "security_group" + role: # a single role is expected [OPTIONAL]. Eg. "roles" defaults: # For some attributes defaults can be given. This will be used if SAML2 response doesn't have them organization-code: NOVA # default organization unit code security-group-code: GRP_USER # default security group code diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml index 3695f21..f0650bf 100644 --- a/src/main/resources/config/application-prod.yml +++ b/src/main/resources/config/application-prod.yml @@ -18,7 +18,9 @@ logging: ROOT: INFO tech.jhipster: INFO com.oguerreiro.resilient: INFO - + org.hibernate.proxy: TRACE + org.hibernate.bytecode: TRACE + management: prometheus: metrics: @@ -33,9 +35,9 @@ spring: enabled: false datasource: type: com.zaxxer.hikari.HikariDataSource - url: jdbc:mysql://localhost:3306/resilient?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&createDatabaseIfNotExist=true + url: jdbc:mysql://localhost:3306/resilient_resilient?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&createDatabaseIfNotExist=true username: root - password: + password: root hikari: poolName: Hikari auto-commit: false diff --git a/src/main/webapp/app/core/auth/account.service.ts b/src/main/webapp/app/core/auth/account.service.ts index 522a3e9..e3fcc06 100644 --- a/src/main/webapp/app/core/auth/account.service.ts +++ b/src/main/webapp/app/core/auth/account.service.ts @@ -3,7 +3,7 @@ import { Router } from '@angular/router'; import { HttpClient } from '@angular/common/http'; import { TranslateService } from '@ngx-translate/core'; import { Observable, ReplaySubject, of } from 'rxjs'; -import { shareReplay, tap, catchError } from 'rxjs/operators'; +import { shareReplay, tap, catchError, map } from 'rxjs/operators'; import { StateStorageService } from 'app/core/auth/state-storage.service'; import { Account } from 'app/core/auth/account.model'; @@ -95,6 +95,14 @@ export class AccountService { return this.http.get(this.applicationConfigService.getEndpointFor('api/account/saml2-endpoint'), { responseType: 'text' as 'text' }); } + hasBasicAuth(): Observable { + return this.http.get( + this.applicationConfigService.getEndpointFor('api/account/has-basic-auth'), { responseType: 'text' as 'text' } + ).pipe( + map(response => response === 'true') + );; + } + private fetch(): Observable { return this.http.get(this.applicationConfigService.getEndpointFor('api/account')); } diff --git a/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-accordion-table.component.ts b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-accordion-table.component.ts index 0083c5c..d7b0983 100644 --- a/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-accordion-table.component.ts +++ b/src/main/webapp/app/entities/dashboard-component/factory/widgets/dashboard-component-accordion-table.component.ts @@ -91,12 +91,12 @@ export class DashboardComponentAccordionTable implements OnInit { } } }); - - // Listen to changes in PeriodSelector + + // Get the currently selected ORGANIZATION AND listen to changes in PeriodSelector this.subscriptionPeriod = this .envService .selectedPeriod - .pipe( skip(1)) // Ignore the current value. Just want to react to changes + /* .pipe( skip(1)) // Ignore's the current value. If you want to react ONLY to changes */ .subscribe(period => { if(period){ // Calculate the latest periodVersionId @@ -105,9 +105,10 @@ export class DashboardComponentAccordionTable implements OnInit { .subscribe({ next: (version: HttpResponse) => { if (version.body) { - const periodVersion = version.body; - this.periodVersionId = periodVersion.id; + this.periodVersion = version.body; + this.periodVersionId = this.periodVersion.id; } else { + this.periodVersion = null; this.periodVersionId = null; } diff --git a/src/main/webapp/app/entities/dashboard/dashboard.routes.ts b/src/main/webapp/app/entities/dashboard/dashboard.routes.ts index 676c2f0..493e2f1 100644 --- a/src/main/webapp/app/entities/dashboard/dashboard.routes.ts +++ b/src/main/webapp/app/entities/dashboard/dashboard.routes.ts @@ -6,6 +6,14 @@ import { DashboardPreviewComponent } from './preview/dashboard-preview.component // import DashboardResolve from './route/dashboard-routing-resolve.service'; const dashboardRoute: Routes = [ + { + path: ':view', + component: DashboardPreviewComponent, + canActivate: [UserRouteAccessService], + data: { + authorities: ['ROLE_ADMIN', 'ROLE_MANAGER', 'ROLE_COORDINATOR', 'ROLE_USER'], + }, + }, { path: ':view/:period', component: DashboardPreviewComponent, diff --git a/src/main/webapp/app/entities/dashboard/preview/dashboard-preview.component.ts b/src/main/webapp/app/entities/dashboard/preview/dashboard-preview.component.ts index 56f9353..95de022 100644 --- a/src/main/webapp/app/entities/dashboard/preview/dashboard-preview.component.ts +++ b/src/main/webapp/app/entities/dashboard/preview/dashboard-preview.component.ts @@ -23,10 +23,9 @@ export class DashboardPreviewComponent implements OnInit, AfterViewInit { @ViewChild('dashboardContainer', { read: ViewContainerRef }) container!: ViewContainerRef; dashboardComponents?: IDashboardComponent[]; - periodsSharedCollection: IPeriod[] = []; dashboardView: string | null = null; - periodId: number = 0; + periodId: number | null = null; periodVersionId: number | null = null; isLoading = false; @@ -54,8 +53,11 @@ export class DashboardPreviewComponent implements OnInit, AfterViewInit { this.dashboardView = params.get('view') ?? null; // used the operator (+) that auto-casts a string to a number - this.periodId = +(params.get('period') ?? 0); - + // this.periodId = +(params.get('period') ?? 0); + if (params.get('period') != null) { + this.periodId = +(params.get('period')!); + } + // periodVersion might not be defined. In this case, it will calculate the most recent version if (params.get('period_version') != null) { this.periodVersionId = +(params.get('period_version')!); @@ -68,8 +70,12 @@ export class DashboardPreviewComponent implements OnInit, AfterViewInit { this.build(); }); - // Get the latest PeriodVersion for the requested Period - if (this.periodVersionId == null) { + if (this.periodId == null) { + // Get the latest Period + PeriodVersion + // Do nothing. The period selector will auto-select the moust recent period + + } else if (this.periodId && this.periodVersionId == null) { + // Get the latest PeriodVersion for the requested Period this.periodService .lastVersion(this.periodId) .subscribe({ @@ -89,8 +95,6 @@ export class DashboardPreviewComponent implements OnInit, AfterViewInit { //Fire Loading of the dashboard this.build(); } - - this.loadRelationshipsOptions(); } onSearch(): void { @@ -102,11 +106,14 @@ export class DashboardPreviewComponent implements OnInit, AfterViewInit { } build(): void { - if (this.periodVersionId == null) { - // Can't load dashboard without a version + // ALWAYS load, even if no period is selected. + /* + if (this.periodId == null || this.periodVersionId == null) { + // Can't load dashboard without a period || periodVersion this.dashboardComponents = []; // Clear components return; } + */ // Load a list of dashboarComponent's to render this.loadDashboards().subscribe({ @@ -141,15 +148,6 @@ export class DashboardPreviewComponent implements OnInit, AfterViewInit { } } - protected loadRelationshipsOptions(): void { - this.periodService - .query() - .pipe(map((res: HttpResponse) => res.body ?? [])) - .subscribe((periods: IPeriod[]) => { - this.periodsSharedCollection = periods; - }); - } - protected setPageTitles(view: string | null): void { this.pageTitle = ''; this.pageSubTitle = ''; diff --git a/src/main/webapp/app/layouts/main/banner/banner.component.html b/src/main/webapp/app/layouts/main/banner/banner.component.html index 49ea24c..2e52f7e 100644 --- a/src/main/webapp/app/layouts/main/banner/banner.component.html +++ b/src/main/webapp/app/layouts/main/banner/banner.component.html @@ -1,6 +1,10 @@
-
- Route Zero +
+ Route Zero + + + InNOVA NOVA Information System on Environment and Sustainability +
\ No newline at end of file diff --git a/src/main/webapp/app/layouts/navbar/navbar.component.html b/src/main/webapp/app/layouts/navbar/navbar.component.html index 0e3caf2..e4c2b7b 100644 --- a/src/main/webapp/app/layouts/navbar/navbar.component.html +++ b/src/main/webapp/app/layouts/navbar/navbar.component.html @@ -45,7 +45,7 @@ > - Organization + Organização
  • @@ -160,7 +160,7 @@ (click)="collapseNavbar()" > - Period + Período
  • @@ -175,7 +175,7 @@ (click)="collapseNavbar()" > - Unit Type + Tipos de Unidade
  • @@ -187,7 +187,7 @@ (click)="collapseNavbar()" > - Unit + Unidade
  • @@ -199,7 +199,7 @@ (click)="collapseNavbar()" > - Unit Converter + conversor de Unidades
  • @@ -214,7 +214,7 @@ (click)="collapseNavbar()" > - Variable Scope + Âmbito
  • @@ -226,7 +226,7 @@ (click)="collapseNavbar()" > - Variable Category + Categoria
  • @@ -238,7 +238,7 @@ (click)="collapseNavbar()" > - Variable + Variavel
  • @@ -261,7 +261,7 @@ (click)="collapseNavbar()" > - Input Data Upload + Ficheiros de Dados
  • - Input Data + Dados de Inventário
  • - Organization Type + Tipo de Organização
  • @@ -343,7 +343,7 @@ (click)="collapseNavbar()" > - Metadata Property + Propriedades Metadata
  • @@ -367,7 +367,7 @@ (click)="collapseNavbar()" > - Variable Class Type + Tipo de Classe
  • @@ -379,7 +379,7 @@ (click)="collapseNavbar()" > - Emission Factors + Fatores de Emissão
  • @@ -391,7 +391,7 @@ (click)="collapseNavbar()" > - Dashboard Comp. + Dashboard-Componente
  • @@ -403,7 +403,7 @@ (click)="collapseNavbar()" > - Document + Documentos
  • @@ -415,7 +415,7 @@ (click)="collapseNavbar()" > - Content Page + Conteúdos
  • diff --git a/src/main/webapp/app/login/login.component.html b/src/main/webapp/app/login/login.component.html index d06f9b4..6dc2af5 100644 --- a/src/main/webapp/app/login/login.component.html +++ b/src/main/webapp/app/login/login.component.html @@ -12,33 +12,35 @@ }
    -
    - +
    +
    + +
    +
    + +
    +

    Esqueci-me da palavra passe

    +
    -
    - -
    -

    Esqueci-me da palavra passe

    - +

    ou

    -

    ou

    diff --git a/src/main/webapp/app/login/login.component.ts b/src/main/webapp/app/login/login.component.ts index cf30f76..e459c8a 100644 --- a/src/main/webapp/app/login/login.component.ts +++ b/src/main/webapp/app/login/login.component.ts @@ -17,6 +17,7 @@ export default class LoginComponent implements OnInit, AfterViewInit { username = viewChild.required('username'); saml2Endpoint : string | null = null; authenticationError = signal(false); + hasBasicAuth: boolean = false; loginForm = new FormGroup({ username: new FormControl('', { nonNullable: true, validators: [Validators.required] }), @@ -44,6 +45,15 @@ export default class LoginComponent implements OnInit, AfterViewInit { console.error('Failed to fetch SAML2 endpoint', err); } }); + + this.accountService.hasBasicAuth().subscribe({ + next: (response: boolean) => { + this.hasBasicAuth = response; + }, + error: (err) => { + console.error('Failed to fetch hasBasicAuth', err); + } + }); } ngAfterViewInit(): void { diff --git a/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector.component.ts b/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector.component.ts index add08a3..a7a0e8a 100644 --- a/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector.component.ts +++ b/src/main/webapp/app/resilient/resilient-environment/period-selector/period-selector.component.ts @@ -63,6 +63,7 @@ export class PeriodSelectorComponent implements OnInit { const queryObject: any = { eagerload: true, sort: this.sortService.buildSortParam(this.sortState()), + }; return this.periodService.query(queryObject).pipe(tap(() => (this.isLoading = false))); } @@ -71,6 +72,14 @@ export class PeriodSelectorComponent implements OnInit { const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); this.periods = this.refineData(dataFromBody); if (this.periods.length >= 1) { + // Sort the periods + this.periods.sort((a, b) => { + if (!a.beginDate && !b.beginDate) return 0; + if (!a.beginDate) return 1; // null dates go last + if (!b.beginDate) return -1; + return b.beginDate.valueOf() - a.beginDate.valueOf(); + }); + if (this.defaultSelectedPeriodId) { // Select the provided default Period this.selectedPeriod = this.periods.find(period => period.id === this.defaultSelectedPeriodId) || undefined; diff --git a/src/main/webapp/content/images/Graphic1_2024.png b/src/main/webapp/content/images/Graphic1_2024.png new file mode 100644 index 0000000..6cea7ab Binary files /dev/null and b/src/main/webapp/content/images/Graphic1_2024.png differ diff --git a/src/main/webapp/content/images/Graphic2_2024.png b/src/main/webapp/content/images/Graphic2_2024.png new file mode 100644 index 0000000..598ec4c Binary files /dev/null and b/src/main/webapp/content/images/Graphic2_2024.png differ diff --git a/src/main/webapp/content/images/Graphic3_2024.png b/src/main/webapp/content/images/Graphic3_2024.png new file mode 100644 index 0000000..9fbec69 Binary files /dev/null and b/src/main/webapp/content/images/Graphic3_2024.png differ diff --git a/src/main/webapp/content/images/Graphic4_2024.png b/src/main/webapp/content/images/Graphic4_2024.png new file mode 100644 index 0000000..4b6920e Binary files /dev/null and b/src/main/webapp/content/images/Graphic4_2024.png differ diff --git a/src/main/webapp/content/images/Graphic_gases_2024.png b/src/main/webapp/content/images/Graphic_gases_2024.png new file mode 100644 index 0000000..cd7c99c Binary files /dev/null and b/src/main/webapp/content/images/Graphic_gases_2024.png differ diff --git a/src/main/webapp/content/images/Graphic_missoes_globais_2024.png b/src/main/webapp/content/images/Graphic_missoes_globais_2024.png new file mode 100644 index 0000000..4899991 Binary files /dev/null and b/src/main/webapp/content/images/Graphic_missoes_globais_2024.png differ