diff --git a/backend/core/src/main/java/org/opencdmp/commons/types/actionconfirmation/PlanInvitationEntity.java b/backend/core/src/main/java/org/opencdmp/commons/types/actionconfirmation/PlanInvitationEntity.java index d957dc264..f542d0bbc 100644 --- a/backend/core/src/main/java/org/opencdmp/commons/types/actionconfirmation/PlanInvitationEntity.java +++ b/backend/core/src/main/java/org/opencdmp/commons/types/actionconfirmation/PlanInvitationEntity.java @@ -18,6 +18,9 @@ public class PlanInvitationEntity { @XmlAttribute(name = "plan-role") private PlanUserRole role; + @XmlAttribute(name = "section") + private UUID sectionId; + public String getEmail() { return email; } @@ -41,4 +44,12 @@ public class PlanInvitationEntity { public void setRole(PlanUserRole role) { this.role = role; } + + public UUID getSectionId() { + return sectionId; + } + + public void setSectionId(UUID sectionId) { + this.sectionId = sectionId; + } } diff --git a/backend/core/src/main/java/org/opencdmp/model/PlanInvitationResult.java b/backend/core/src/main/java/org/opencdmp/model/PlanInvitationResult.java new file mode 100644 index 000000000..9f9c5ae5c --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/model/PlanInvitationResult.java @@ -0,0 +1,26 @@ +package org.opencdmp.model; + +import java.util.UUID; + +public class PlanInvitationResult { + + private UUID planId; + + private Boolean isAlreadyAccepted; + + public UUID getPlanId() { + return planId; + } + + public void setPlanId(UUID planId) { + this.planId = planId; + } + + public Boolean getIsAlreadyAccepted() { + return isAlreadyAccepted; + } + + public void setIsAlreadyAccepted(Boolean alreadyAccepted) { + isAlreadyAccepted = alreadyAccepted; + } +} diff --git a/backend/core/src/main/java/org/opencdmp/model/actionconfirmation/PlanInvitation.java b/backend/core/src/main/java/org/opencdmp/model/actionconfirmation/PlanInvitation.java index 9224208e4..f6d2121a8 100644 --- a/backend/core/src/main/java/org/opencdmp/model/actionconfirmation/PlanInvitation.java +++ b/backend/core/src/main/java/org/opencdmp/model/actionconfirmation/PlanInvitation.java @@ -12,6 +12,9 @@ public class PlanInvitation { private UUID planId; public static final String _planId = "planId"; + private UUID sectionId; + public static final String _sectionId = "sectionId"; + private PlanUserRole role; public static final String _role = "role"; @@ -32,6 +35,14 @@ public class PlanInvitation { this.planId = planId; } + public UUID getSectionId() { + return sectionId; + } + + public void setSectionId(UUID sectionId) { + this.sectionId = sectionId; + } + public PlanUserRole getRole() { return role; } diff --git a/backend/core/src/main/java/org/opencdmp/model/builder/actionconfirmation/PlanInvitationBuilder.java b/backend/core/src/main/java/org/opencdmp/model/builder/actionconfirmation/PlanInvitationBuilder.java index a8fa1163c..0c8bcb36e 100644 --- a/backend/core/src/main/java/org/opencdmp/model/builder/actionconfirmation/PlanInvitationBuilder.java +++ b/backend/core/src/main/java/org/opencdmp/model/builder/actionconfirmation/PlanInvitationBuilder.java @@ -47,6 +47,7 @@ public class PlanInvitationBuilder extends BaseBuilder users) throws InvalidApplicationException, JAXBException, IOException; - UUID planInvitationAccept(String token) throws InvalidApplicationException, IOException; + PlanInvitationResult planInvitationAccept(String token) throws InvalidApplicationException, IOException; PlanImportExport exportXmlEntity(UUID id, boolean ignoreAuthorize, boolean isPublic) throws MyForbiddenException, MyNotFoundException, JAXBException, ParserConfigurationException, IOException, InstantiationException, IllegalAccessException, SAXException, InvalidApplicationException; diff --git a/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java index d39b3db91..3e2a6ef59 100644 --- a/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/plan/PlanServiceImpl.java @@ -69,10 +69,7 @@ import org.opencdmp.integrationevent.outbox.annotationentityremoval.AnnotationEn import org.opencdmp.integrationevent.outbox.annotationentitytouch.AnnotationEntityTouchedIntegrationEventHandler; import org.opencdmp.integrationevent.outbox.notification.NotifyIntegrationEvent; import org.opencdmp.integrationevent.outbox.notification.NotifyIntegrationEventHandler; -import org.opencdmp.model.PlanDescriptionTemplate; -import org.opencdmp.model.PlanUser; -import org.opencdmp.model.PlanValidationResult; -import org.opencdmp.model.PublicPlan; +import org.opencdmp.model.*; import org.opencdmp.model.builder.PlanUserBuilder; import org.opencdmp.model.builder.description.DescriptionBuilder; import org.opencdmp.model.builder.plan.PlanBuilder; @@ -1975,7 +1972,7 @@ public class PlanServiceImpl implements PlanService { this.sendPlanInvitationExistingUser(user.getUser(), plan, user.getRole()); } }else if (user.getEmail() != null) { - this.sendPlanInvitationExternalUser(user.getEmail(),plan, user.getRole()); + this.sendPlanInvitationExternalUser(user.getEmail(),plan, user.getRole(), user.getSectionId()); } } @@ -2013,8 +2010,8 @@ public class PlanServiceImpl implements PlanService { this.eventHandler.handle(event); } - private void sendPlanInvitationExternalUser(String email, PlanEntity plan, PlanUserRole role) throws JAXBException, InvalidApplicationException { - String token = this.createActionConfirmation(email, plan, role); + private void sendPlanInvitationExternalUser(String email, PlanEntity plan, PlanUserRole role, UUID sectionId) throws JAXBException, InvalidApplicationException { + String token = this.createActionConfirmation(email, plan, role, sectionId); NotifyIntegrationEvent event = new NotifyIntegrationEvent(); @@ -2038,12 +2035,12 @@ public class PlanServiceImpl implements PlanService { this.eventHandler.handle(event); } - private String createActionConfirmation(String email, PlanEntity plan, PlanUserRole role) throws JAXBException, InvalidApplicationException { + private String createActionConfirmation(String email, PlanEntity plan, PlanUserRole role, UUID sectionId) throws JAXBException, InvalidApplicationException { ActionConfirmationPersist persist = new ActionConfirmationPersist(); persist.setType(ActionConfirmationType.PlanInvitation); persist.setStatus(ActionConfirmationStatus.Requested); persist.setToken(UUID.randomUUID().toString()); - persist.setPlanInvitation(new PlanInvitationPersist(email, plan.getId(), role)); + persist.setPlanInvitation(new PlanInvitationPersist(email, plan.getId(), sectionId, role)); persist.setExpiresAt(Instant.now().plusSeconds(this.notificationProperties.getEmailExpirationTimeSeconds())); this.validatorFactory.validator(ActionConfirmationPersist.ActionConfirmationPersistValidator.class).validateForce(persist); this.actionConfirmationService.persist(persist, null); @@ -2051,23 +2048,29 @@ public class PlanServiceImpl implements PlanService { return persist.getToken(); } - public UUID planInvitationAccept(String token) throws InvalidApplicationException { + public PlanInvitationResult planInvitationAccept(String token) throws InvalidApplicationException { + PlanInvitationResult planInvitationResult = new PlanInvitationResult(); ActionConfirmationEntity action = this.queryFactory.query(ActionConfirmationQuery.class).tokens(token).types(ActionConfirmationType.PlanInvitation).isActive(IsActive.Active).first(); if (action == null){ throw new MyValidationException(this.errors.getTokenNotExist().getCode(), this.errors.getTokenNotExist().getMessage()); } - if (action.getStatus().equals(ActionConfirmationStatus.Accepted)){ - throw new MyValidationException(this.errors.getPlanInvitationAlreadyConfirmed().getCode(), this.errors.getPlanInvitationAlreadyConfirmed().getMessage()); - } - if (action.getExpiresAt().compareTo(Instant.now()) < 0){ - throw new MyValidationException(this.errors.getRequestHasExpired().getCode(), this.errors.getRequestHasExpired().getMessage()); - } - PlanInvitationEntity planInvitation = this.xmlHandlingService.fromXmlSafe(PlanInvitationEntity.class, action.getData()); if (planInvitation == null) { throw new MyApplicationException("plan invitation don't exist"); } + if (action.getStatus().equals(ActionConfirmationStatus.Accepted)){ + UserContactInfoEntity contactInfoEntity = this.queryFactory.query(UserContactInfoQuery.class).disableTracking().userIds(this.userScope.getUserId()).values(planInvitation.getEmail()).types(ContactInfoType.Email).first(); + if (contactInfoEntity == null){ + throw new MyValidationException(this.errors.getAnotherUserToken().getCode(), this.errors.getAnotherUserToken().getMessage()); + } + planInvitationResult.setPlanId(planInvitation.getPlanId()); + planInvitationResult.setIsAlreadyAccepted(true); + return planInvitationResult; + } + if (action.getExpiresAt().compareTo(Instant.now()) < 0){ + throw new MyValidationException(this.errors.getRequestHasExpired().getCode(), this.errors.getRequestHasExpired().getMessage()); + } UserContactInfoEntity contactInfoEntity = this.queryFactory.query(UserContactInfoQuery.class).disableTracking().userIds(this.userScope.getUserId()).values(planInvitation.getEmail()).types(ContactInfoType.Email).first(); if (contactInfoEntity == null){ @@ -2081,13 +2084,16 @@ public class PlanServiceImpl implements PlanService { data.setRole(planInvitation.getRole()); data.setUserId(this.userScope.getUserIdSafe()); data.setPlanId(planInvitation.getPlanId()); + data.setSectionId(planInvitation.getSectionId()); this.entityManager.persist(data); action.setStatus(ActionConfirmationStatus.Accepted); this.entityManager.merge(action); this.annotationEntityTouchedIntegrationEventHandler.handlePlan(planInvitation.getPlanId()); - return planInvitation.getPlanId(); + planInvitationResult.setPlanId(planInvitation.getPlanId()); + planInvitationResult.setIsAlreadyAccepted(false); + return planInvitationResult; } //region Export diff --git a/backend/web/src/main/java/org/opencdmp/controllers/PlanController.java b/backend/web/src/main/java/org/opencdmp/controllers/PlanController.java index f95a2c515..b20b4e7e0 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/PlanController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/PlanController.java @@ -31,10 +31,7 @@ import org.opencdmp.controllers.swagger.annotation.Swagger400; import org.opencdmp.controllers.swagger.annotation.Swagger404; import org.opencdmp.controllers.swagger.annotation.SwaggerCommonErrorResponses; import org.opencdmp.filetransformerbase.models.misc.PreprocessingPlanModel; -import org.opencdmp.model.DescriptionsToBeFinalized; -import org.opencdmp.model.PlanUser; -import org.opencdmp.model.PlanValidationResult; -import org.opencdmp.model.PublicPlan; +import org.opencdmp.model.*; import org.opencdmp.model.builder.PublicPlanBuilder; import org.opencdmp.model.builder.plan.PlanBuilder; import org.opencdmp.model.censorship.PublicPlanCensor; @@ -469,16 +466,16 @@ public class PlanController { @OperationWithTenantHeader(summary = "Accept an invitation token for a plan by token") @Transactional @Hidden - public UUID acceptInvitation(@PathVariable("token") String token) throws InvalidApplicationException, JAXBException, IOException { + public PlanInvitationResult acceptInvitation(@PathVariable("token") String token) throws InvalidApplicationException, JAXBException, IOException { logger.debug(new MapLogEntry("inviting users to plan").And("token", token)); - UUID planId = this.planService.planInvitationAccept(token); + PlanInvitationResult planInvitationResult = this.planService.planInvitationAccept(token); this.auditService.track(AuditableAction.Plan_Invite_Accept, Map.ofEntries( new AbstractMap.SimpleEntry("token", token) )); - return planId; + return planInvitationResult; } @RequestMapping(method = RequestMethod.GET, value = "/xml/export/{id}", produces = "application/xml") diff --git a/frontend/src/app/core/model/plan/plan.ts b/frontend/src/app/core/model/plan/plan.ts index c2a0ca34f..edc5913b9 100644 --- a/frontend/src/app/core/model/plan/plan.ts +++ b/frontend/src/app/core/model/plan/plan.ts @@ -183,5 +183,10 @@ export interface PlanUserInvitePersist { } // -// Public -// \ No newline at end of file +// Invitation +// + +export interface PlanInvitationResult { + planId: Guid; + isAlreadyAccepted: boolean; +} \ No newline at end of file diff --git a/frontend/src/app/core/services/plan/plan.service.ts b/frontend/src/app/core/services/plan/plan.service.ts index 7f8169103..fbbede8e4 100644 --- a/frontend/src/app/core/services/plan/plan.service.ts +++ b/frontend/src/app/core/services/plan/plan.service.ts @@ -15,7 +15,7 @@ import { catchError, map } from 'rxjs/operators'; import { nameof } from 'ts-simple-nameof'; import { BaseHttpParams } from '../../../../common/http/base-http-params'; import { InterceptorType } from '../../../../common/http/interceptors/interceptor-type'; -import { ClonePlanPersist, Plan, PlanPersist, PlanUser, PlanUserInvitePersist, PlanUserPersist, PlanUserRemovePersist, NewVersionPlanPersist, PublicPlan } from '../../model/plan/plan'; +import { ClonePlanPersist, Plan, PlanPersist, PlanUser, PlanUserInvitePersist, PlanUserPersist, PlanUserRemovePersist, NewVersionPlanPersist, PublicPlan, PlanInvitationResult } from '../../model/plan/plan'; import { AuthService } from '../auth/auth.service'; import { ConfigurationService } from '../configuration/configuration.service'; import { BaseHttpV2Service } from '../http/base-http-v2.service'; @@ -162,10 +162,10 @@ export class PlanService { catchError((error: any) => throwError(error))); } - acceptInvitation(token: string): Observable { + acceptInvitation(token: string): Observable { const url = `${this.apiBase}/token/${token}/invite-accept`; - return this.http.get(url).pipe(catchError((error: any) => throwError(error))); + return this.http.get(url).pipe(catchError((error: any) => throwError(error))); } downloadXML(id: Guid): Observable> { diff --git a/frontend/src/app/ui/plan/invitation/accepted/plan-invitation-accepted.component.ts b/frontend/src/app/ui/plan/invitation/accepted/plan-invitation-accepted.component.ts index 3a5f3acd4..eae1d2d84 100644 --- a/frontend/src/app/ui/plan/invitation/accepted/plan-invitation-accepted.component.ts +++ b/frontend/src/app/ui/plan/invitation/accepted/plan-invitation-accepted.component.ts @@ -2,11 +2,12 @@ import { HttpErrorResponse } from '@angular/common/http'; import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { AuthService } from '@app/core/services/auth/auth.service'; -import { SnackBarNotificationLevel } from '@app/core/services/notification/ui-notification-service'; +import { SnackBarNotificationLevel, UiNotificationService } from '@app/core/services/notification/ui-notification-service'; import { PlanService } from '@app/core/services/plan/plan.service'; import { RouterUtilsService } from '@app/core/services/router/router-utils.service'; import { BaseComponent } from '@common/base/base.component'; import { HttpError, HttpErrorHandlingService } from '@common/modules/errors/error-handling/http-error-handling.service'; +import { TranslateService } from '@ngx-translate/core'; import { takeUntil } from 'rxjs/operators'; @Component({ @@ -21,7 +22,9 @@ export class InvitationAcceptedComponent extends BaseComponent implements OnInit private router: Router, private authentication: AuthService, protected routerUtils: RouterUtilsService, - private httpErrorHandlingService: HttpErrorHandlingService + protected language: TranslateService, + private httpErrorHandlingService: HttpErrorHandlingService, + private uiNotificationService: UiNotificationService ) { super(); } ngOnInit(): void { @@ -35,7 +38,10 @@ export class InvitationAcceptedComponent extends BaseComponent implements OnInit this.planService.acceptInvitation(token) .pipe(takeUntil(this._destroyed)) .subscribe(result => { - this.router.navigate([this.routerUtils.generateUrl('plans/edit/' + result)]); + if (result?.isAlreadyAccepted == false) { + this.uiNotificationService.snackBarNotification(this.language.instant('PLAN-USER-INVITATION-DIALOG.SUCCESS'), SnackBarNotificationLevel.Success); + } + this.router.navigate([this.routerUtils.generateUrl('plans/overview/' + result?.planId)]); }, error => this.onCallbackError(error)); }else{