diff --git a/annotation-service/annotation/src/main/java/gr/cite/annotation/authorization/authorizationcontentresolver/AuthorizationContentResolverImpl.java b/annotation-service/annotation/src/main/java/gr/cite/annotation/authorization/authorizationcontentresolver/AuthorizationContentResolverImpl.java index 67b6eed97..73e477e8d 100644 --- a/annotation-service/annotation/src/main/java/gr/cite/annotation/authorization/authorizationcontentresolver/AuthorizationContentResolverImpl.java +++ b/annotation-service/annotation/src/main/java/gr/cite/annotation/authorization/authorizationcontentresolver/AuthorizationContentResolverImpl.java @@ -81,11 +81,11 @@ public class AuthorizationContentResolverImpl implements AuthorizationContentRes List annotationEntities = this.queryFactory.query(AnnotationQuery.class).disableTracking().ids(ids).collectAs(new BaseFieldSet().ensure(Annotation._id).ensure(Annotation._entityId).ensure(Annotation._id)); List entityUsers = this.queryFactory.query(EntityUserQuery.class).disableTracking().entityIds(annotationEntities.stream().map(AnnotationEntity::getEntityId).distinct().toList()).userIds(userId).isActive(IsActive.Active).collectAs(new BaseFieldSet().ensure(EntityUser._id).ensure(EntityUser._entityId)); - Map> dmpUsersMap = entityUsers.stream().collect(Collectors.groupingBy(EntityUserEntity::getEntityId)); + Map> entityUsersMap = entityUsers.stream().collect(Collectors.groupingBy(EntityUserEntity::getEntityId)); for (AnnotationEntity annotation : annotationEntities){ - List dmpDescriptionUsers = dmpUsersMap.getOrDefault(annotation.getEntityId(), new ArrayList<>()); - if (!dmpDescriptionUsers.isEmpty()) { + List annotationEntityUsers = entityUsersMap.getOrDefault(annotation.getEntityId(), new ArrayList<>()); + if (!annotationEntityUsers.isEmpty()) { affiliatedResources.get(annotation.getId()).setAffiliated(true); } } diff --git a/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java b/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java index 907adb9f4..c498f903d 100644 --- a/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java +++ b/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java @@ -104,6 +104,8 @@ public class AuditableAction { public static final EventId User_RemoveCredentialConfirm = new EventId(11013, "User_RemoveCredentialConfirm"); public static final EventId User_DmpAssociatedQuery = new EventId(11014, "User_DmpAssociatedQuery"); public static final EventId User_AllowMergeAccount = new EventId(11015, "User_AllowMergeAccount"); + public static final EventId User_InviteToTenant = new EventId(11016, "User_InviteToTenant"); + public static final EventId User_InviteToTenantConfirm = new EventId(11017, "User_InviteToTenantConfirm"); public static final EventId Tenant_Query = new EventId(12000, "Tenant_Query"); public static final EventId Tenant_Lookup = new EventId(12001, "Tenant_Lookup"); diff --git a/backend/core/src/main/java/org/opencdmp/commons/enums/ActionConfirmationType.java b/backend/core/src/main/java/org/opencdmp/commons/enums/ActionConfirmationType.java index 37ac7b759..237ab3f7a 100644 --- a/backend/core/src/main/java/org/opencdmp/commons/enums/ActionConfirmationType.java +++ b/backend/core/src/main/java/org/opencdmp/commons/enums/ActionConfirmationType.java @@ -9,7 +9,8 @@ public enum ActionConfirmationType implements DatabaseEnum { MergeAccount((short) 0), RemoveCredential((short) 1), - DmpInvitation((short) 2); + DmpInvitation((short) 2), + UserInviteToTenant ((short) 3); private final Short value; diff --git a/backend/core/src/main/java/org/opencdmp/commons/notification/NotificationProperties.java b/backend/core/src/main/java/org/opencdmp/commons/notification/NotificationProperties.java index c442c755b..651eed085 100644 --- a/backend/core/src/main/java/org/opencdmp/commons/notification/NotificationProperties.java +++ b/backend/core/src/main/java/org/opencdmp/commons/notification/NotificationProperties.java @@ -21,6 +21,7 @@ public class NotificationProperties { private UUID descriptionTemplateInvitationType; private UUID contactSupportType; private UUID publicContactSupportType; + private UUID tenantSpecificInvitationUserType; private int emailExpirationTimeSeconds; private String contactSupportEmail; @@ -151,4 +152,12 @@ public class NotificationProperties { public void setDescriptionAnnotationCreated(UUID descriptionAnnotationCreated) { this.descriptionAnnotationCreated = descriptionAnnotationCreated; } + + public UUID getTenantSpecificInvitationUserType() { + return tenantSpecificInvitationUserType; + } + + public void setTenantSpecificInvitationUserType(UUID tenantSpecificInvitationUserType) { + this.tenantSpecificInvitationUserType = tenantSpecificInvitationUserType; + } } diff --git a/backend/core/src/main/java/org/opencdmp/commons/types/actionconfirmation/UserInviteToTenantRequestEntity.java b/backend/core/src/main/java/org/opencdmp/commons/types/actionconfirmation/UserInviteToTenantRequestEntity.java new file mode 100644 index 000000000..707f712a1 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/commons/types/actionconfirmation/UserInviteToTenantRequestEntity.java @@ -0,0 +1,48 @@ +package org.opencdmp.commons.types.actionconfirmation; + +import jakarta.xml.bind.annotation.*; + +import java.util.List; + +@XmlRootElement(name = "user-invite-to-tenant-confirmation") +@XmlAccessorType(XmlAccessType.FIELD) +public class UserInviteToTenantRequestEntity { + + @XmlAttribute(name = "email") + private String email; + + @XmlAttribute(name = "tenantCode") + private String tenantCode; + + @XmlElementWrapper(name = "roles") + @XmlElement(name = "role") + private List roles; + + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getTenantCode() { + return tenantCode; + } + + public void setTenantCode(String tenantCode) { + this.tenantCode = tenantCode; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + +} + diff --git a/backend/core/src/main/java/org/opencdmp/model/actionconfirmation/ActionConfirmation.java b/backend/core/src/main/java/org/opencdmp/model/actionconfirmation/ActionConfirmation.java index 38aa694f1..b41a3547e 100644 --- a/backend/core/src/main/java/org/opencdmp/model/actionconfirmation/ActionConfirmation.java +++ b/backend/core/src/main/java/org/opencdmp/model/actionconfirmation/ActionConfirmation.java @@ -26,6 +26,10 @@ public class ActionConfirmation { public static final String _removeCredentialRequest = "removeCredentialRequest"; + private UserInviteToTenantRequest userInviteToTenantRequest; + + public static final String _userInviteToTenantRequest = "userInviteToTenantRequest"; + private DmpInvitation dmpInvitation; public static final String _dmpInvitation = "dmpInvitation"; @@ -154,4 +158,12 @@ public class ActionConfirmation { public void setRemoveCredentialRequest(RemoveCredentialRequest removeCredentialRequest) { this.removeCredentialRequest = removeCredentialRequest; } + + public UserInviteToTenantRequest getUserInviteToTenantRequest() { + return userInviteToTenantRequest; + } + + public void setUserInviteToTenantRequest(UserInviteToTenantRequest userInviteToTenantRequest) { + this.userInviteToTenantRequest = userInviteToTenantRequest; + } } diff --git a/backend/core/src/main/java/org/opencdmp/model/actionconfirmation/UserInviteToTenantRequest.java b/backend/core/src/main/java/org/opencdmp/model/actionconfirmation/UserInviteToTenantRequest.java new file mode 100644 index 000000000..708ccddc0 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/model/actionconfirmation/UserInviteToTenantRequest.java @@ -0,0 +1,42 @@ +package org.opencdmp.model.actionconfirmation; + +import java.util.List; + +public class UserInviteToTenantRequest { + + private String email; + public static final String _email = "email"; + + private String tenantCode; + public static final String _tenantCode = "tenantCode"; + + private List roles; + public static final String _roles = "roles"; + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getTenantCode() { + return tenantCode; + } + + public void setTenantCode(String tenantCode) { + this.tenantCode = tenantCode; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + +} + diff --git a/backend/core/src/main/java/org/opencdmp/model/builder/actionconfirmation/ActionConfirmationBuilder.java b/backend/core/src/main/java/org/opencdmp/model/builder/actionconfirmation/ActionConfirmationBuilder.java index bd5de2a7a..5f22f13f0 100644 --- a/backend/core/src/main/java/org/opencdmp/model/builder/actionconfirmation/ActionConfirmationBuilder.java +++ b/backend/core/src/main/java/org/opencdmp/model/builder/actionconfirmation/ActionConfirmationBuilder.java @@ -13,6 +13,7 @@ import org.opencdmp.commons.scope.tenant.TenantScope; import org.opencdmp.commons.types.actionconfirmation.DmpInvitationEntity; import org.opencdmp.commons.types.actionconfirmation.MergeAccountConfirmationEntity; import org.opencdmp.commons.types.actionconfirmation.RemoveCredentialRequestEntity; +import org.opencdmp.commons.types.actionconfirmation.UserInviteToTenantRequestEntity; import org.opencdmp.convention.ConventionService; import org.opencdmp.data.ActionConfirmationEntity; import org.opencdmp.model.actionconfirmation.ActionConfirmation; @@ -60,6 +61,7 @@ public class ActionConfirmationBuilder extends BaseBuilder { + UserInviteToTenantRequestEntity emailConfirmation = this.xmlHandlingService.fromXmlSafe(UserInviteToTenantRequestEntity.class, d.getData()); + m.setUserInviteToTenantRequest(this.builderFactory.builder(UserInviteToTenantRequestBuilder.class).authorize(this.authorize).build(userInviteToTenantRequestFields, emailConfirmation)); + } default -> throw new InternalError("unknown type: " + d.getType()); } diff --git a/backend/core/src/main/java/org/opencdmp/model/builder/actionconfirmation/UserInviteToTenantRequestBuilder.java b/backend/core/src/main/java/org/opencdmp/model/builder/actionconfirmation/UserInviteToTenantRequestBuilder.java new file mode 100644 index 000000000..4578407a2 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/model/builder/actionconfirmation/UserInviteToTenantRequestBuilder.java @@ -0,0 +1,57 @@ +package org.opencdmp.model.builder.actionconfirmation; + +import gr.cite.tools.exception.MyApplicationException; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.DataLogEntry; +import gr.cite.tools.logging.LoggerService; +import org.opencdmp.authorization.AuthorizationFlags; +import org.opencdmp.commons.types.actionconfirmation.UserInviteToTenantRequestEntity; +import org.opencdmp.convention.ConventionService; +import org.opencdmp.model.actionconfirmation.UserInviteToTenantRequest; +import org.opencdmp.model.builder.BaseBuilder; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class UserInviteToTenantRequestBuilder extends BaseBuilder { + + private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); + + @Autowired + public UserInviteToTenantRequestBuilder( + ConventionService conventionService) { + super(conventionService, new LoggerService(LoggerFactory.getLogger(UserInviteToTenantRequestBuilder.class))); + } + + public UserInviteToTenantRequestBuilder authorize(EnumSet values) { + this.authorize = values; + return this; + } + + @Override + public List build(FieldSet fields, List data) throws MyApplicationException { + this.logger.debug("building for {} items requesting {} fields", Optional.ofNullable(data).map(List::size).orElse(0), Optional.ofNullable(fields).map(FieldSet::getFields).map(Set::size).orElse(0)); + this.logger.trace(new DataLogEntry("requested fields", fields)); + if (fields == null || data == null || fields.isEmpty()) + return new ArrayList<>(); + + + List models = new ArrayList<>(); + for (UserInviteToTenantRequestEntity d : data) { + UserInviteToTenantRequest m = new UserInviteToTenantRequest(); + if (fields.hasField(this.asIndexer(UserInviteToTenantRequest._email))) m.setEmail(d.getEmail()); + if (fields.hasField(this.asIndexer(UserInviteToTenantRequest._tenantCode))) m.setTenantCode(d.getTenantCode()); + if (fields.hasField(this.asIndexer(UserInviteToTenantRequest._roles))) m.setRoles(d.getRoles()); + + models.add(m); + } + this.logger.debug("build {} items", Optional.of(models).map(List::size).orElse(0)); + return models; + } +} diff --git a/backend/core/src/main/java/org/opencdmp/model/persist/ActionConfirmationPersist.java b/backend/core/src/main/java/org/opencdmp/model/persist/ActionConfirmationPersist.java index 7165dea41..ed62e9f83 100644 --- a/backend/core/src/main/java/org/opencdmp/model/persist/ActionConfirmationPersist.java +++ b/backend/core/src/main/java/org/opencdmp/model/persist/ActionConfirmationPersist.java @@ -49,6 +49,10 @@ public class ActionConfirmationPersist { private static final String _removeCredentialRequest = "removeCredentialRequest"; + private UserInviteToTenantRequestPersist userInviteToTenantRequestPersist; + + private static final String _userInviteToTenantRequest = "userInviteToTenantRequest"; + private Instant expiresAt; private static final String _expiresAt = "expiresAt"; @@ -105,6 +109,14 @@ public class ActionConfirmationPersist { this.removeCredentialRequest = removeCredentialRequest; } + public UserInviteToTenantRequestPersist getUserInviteToTenantRequest() { + return userInviteToTenantRequestPersist; + } + + public void setUserInviteToTenantRequest(UserInviteToTenantRequestPersist userInviteToTenantRequestPersist) { + this.userInviteToTenantRequestPersist = userInviteToTenantRequestPersist; + } + public String getToken() { return token; } @@ -182,12 +194,14 @@ public class ActionConfirmationPersist { .iff(() -> ActionConfirmationType.DmpInvitation.equals(item.getType())) .must(() -> !this.isNull(item.getDmpInvitation())) .failOn(ActionConfirmationPersist._dmpInvitation).failWith(messageSource.getMessage("Validation_Required", new Object[]{ActionConfirmationPersist._dmpInvitation}, LocaleContextHolder.getLocale())), - this.spec() .iff(() -> ActionConfirmationType.RemoveCredential.equals(item.getType())) .must(() -> !this.isNull(item.getRemoveCredentialRequest())) .failOn(ActionConfirmationPersist._removeCredentialRequest).failWith(messageSource.getMessage("Validation_Required", new Object[]{ActionConfirmationPersist._removeCredentialRequest}, LocaleContextHolder.getLocale())), - + this.spec() + .iff(() -> ActionConfirmationType.UserInviteToTenant.equals(item.getType())) + .must(() -> !this.isNull(item.getUserInviteToTenantRequest())) + .failOn(ActionConfirmationPersist._userInviteToTenantRequest).failWith(messageSource.getMessage("Validation_Required", new Object[]{ActionConfirmationPersist._userInviteToTenantRequest}, LocaleContextHolder.getLocale())), this.refSpec() .iff(() -> !this.isNull(item.getDmpInvitation())) .on(ActionConfirmationPersist._dmpInvitation) @@ -198,12 +212,16 @@ public class ActionConfirmationPersist { .on(ActionConfirmationPersist._mergeAccountConfirmation) .over(item.getMergeAccountConfirmation()) .using(() -> this.validatorFactory.validator(MergeAccountConfirmationPersist.MergeAccountConfirmationPersistValidator.class)), - this.refSpec() .iff(() -> !this.isNull(item.getRemoveCredentialRequest())) .on(ActionConfirmationPersist._removeCredentialRequest) .over(item.getRemoveCredentialRequest()) - .using(() -> this.validatorFactory.validator(RemoveCredentialRequestPersist.RemoveCredentialRequestPersistValidator.class)) + .using(() -> this.validatorFactory.validator(RemoveCredentialRequestPersist.RemoveCredentialRequestPersistValidator.class)), + this.refSpec() + .iff(() -> !this.isNull(item.getUserInviteToTenantRequest())) + .on(ActionConfirmationPersist._userInviteToTenantRequest) + .over(item.getUserInviteToTenantRequest()) + .using(() -> this.validatorFactory.validator(UserInviteToTenantRequestPersist.UserInviteToTenantRequestPersistValidator.class)) ); } diff --git a/backend/core/src/main/java/org/opencdmp/model/persist/UserInviteToTenantRequestPersist.java b/backend/core/src/main/java/org/opencdmp/model/persist/UserInviteToTenantRequestPersist.java new file mode 100644 index 000000000..4a69f719c --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/model/persist/UserInviteToTenantRequestPersist.java @@ -0,0 +1,93 @@ +package org.opencdmp.model.persist; + +import gr.cite.tools.validation.specification.Specification; +import org.opencdmp.commons.validation.BaseValidator; +import org.opencdmp.convention.ConventionService; +import org.opencdmp.errorcode.ErrorThesaurusProperties; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Scope; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; + +public class UserInviteToTenantRequestPersist { + + private String email; + public static final String _email = "email"; + + + private String tenantCode; + public static final String _tenantCode = "tenantCode"; + + private List roles; + public static final String _roles = "roles"; + + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getTenantCode() { + return tenantCode; + } + + public void setTenantCode(String tenantCode) { + this.tenantCode = tenantCode; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + @Component(UserInviteToTenantRequestPersistValidator.ValidatorName) + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + public static class UserInviteToTenantRequestPersistValidator extends BaseValidator { + + public static final String ValidatorName = "UserInviteToTenantRequestPersistValidator"; + + private final MessageSource messageSource; + + + protected UserInviteToTenantRequestPersistValidator(ConventionService conventionService, ErrorThesaurusProperties errors, MessageSource messageSource) { + super(conventionService, errors); + this.messageSource = messageSource; + } + + @Override + protected Class modelClass() { + return UserInviteToTenantRequestPersist.class; + } + + @Override + protected List specifications(UserInviteToTenantRequestPersist item) { + return Arrays.asList( + this.spec() + .must(() -> !this.isEmpty(item.getEmail())) + .failOn(UserInviteToTenantRequestPersist._email).failWith(messageSource.getMessage("Validation_Required", new Object[]{UserInviteToTenantRequestPersist._email}, LocaleContextHolder.getLocale())), + this.spec() + .must(() -> !this.isEmpty(item.getTenantCode())) + .failOn(UserInviteToTenantRequestPersist._tenantCode).failWith(messageSource.getMessage("Validation_Required", new Object[]{UserInviteToTenantRequestPersist._tenantCode}, LocaleContextHolder.getLocale())), + this.spec() + .iff(() -> !this.isEmpty(item.getEmail())) + .must(() -> this.isValidEmail(item.getEmail())) + .failOn(UserInviteToTenantRequestPersist._email).failWith(messageSource.getMessage("Validation_UnexpectedValue", new Object[]{UserInviteToTenantRequestPersist._email}, LocaleContextHolder.getLocale())), + this.spec() + .must(() -> !this.isListNullOrEmpty(item.getRoles())) + .failOn(UserInviteToTenantRequestPersist._roles).failWith(messageSource.getMessage("Validation_Required", new Object[]{UserInviteToTenantRequestPersist._roles}, LocaleContextHolder.getLocale())) + ); + } + } + +} + diff --git a/backend/core/src/main/java/org/opencdmp/model/persist/UserTenantUsersInviteRequest.java b/backend/core/src/main/java/org/opencdmp/model/persist/UserTenantUsersInviteRequest.java new file mode 100644 index 000000000..c3896e014 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/model/persist/UserTenantUsersInviteRequest.java @@ -0,0 +1,67 @@ +package org.opencdmp.model.persist; + +import gr.cite.tools.validation.ValidatorFactory; +import gr.cite.tools.validation.specification.Specification; +import org.opencdmp.commons.validation.BaseValidator; +import org.opencdmp.convention.ConventionService; +import org.opencdmp.errorcode.ErrorThesaurusProperties; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Scope; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; + +public class UserTenantUsersInviteRequest { + + private List users; + public static final String _users = "users"; + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } + + @Component(UserTenantUsersInviteRequestValidator.ValidatorName) + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + public static class UserTenantUsersInviteRequestValidator extends BaseValidator { + + public static final String ValidatorName = "UserTenantUsersInviteRequestValidator"; + + private final ValidatorFactory validatorFactory; + private final MessageSource messageSource; + + + protected UserTenantUsersInviteRequestValidator(ConventionService conventionService, ErrorThesaurusProperties errors, ValidatorFactory validatorFactory, MessageSource messageSource) { + super(conventionService, errors); + this.validatorFactory = validatorFactory; + this.messageSource = messageSource; + } + + @Override + protected Class modelClass() { + return UserTenantUsersInviteRequest.class; + } + + @Override + protected List specifications(UserTenantUsersInviteRequest item) { + return Arrays.asList( + this.spec() + .must(() -> !this.isListNullOrEmpty(item.getUsers())) + .failOn(UserTenantUsersInviteRequest._users).failWith(messageSource.getMessage("Validation_Required", new Object[]{UserTenantUsersInviteRequest._users}, LocaleContextHolder.getLocale())), + this.navSpec() + .iff(() -> !this.isListNullOrEmpty(item.getUsers())) + .on(UserTenantUsersInviteRequest._users) + .over(item.getUsers()) + .using((itm) -> this.validatorFactory.validator(UserInviteToTenantRequestPersist.UserInviteToTenantRequestPersistValidator.class)) + ); + } + } + +} + diff --git a/backend/core/src/main/java/org/opencdmp/service/actionconfirmation/ActionConfirmationServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/actionconfirmation/ActionConfirmationServiceImpl.java index 08808e569..b19c67ed2 100644 --- a/backend/core/src/main/java/org/opencdmp/service/actionconfirmation/ActionConfirmationServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/actionconfirmation/ActionConfirmationServiceImpl.java @@ -21,6 +21,7 @@ import org.opencdmp.commons.scope.user.UserScope; import org.opencdmp.commons.types.actionconfirmation.DmpInvitationEntity; import org.opencdmp.commons.types.actionconfirmation.MergeAccountConfirmationEntity; import org.opencdmp.commons.types.actionconfirmation.RemoveCredentialRequestEntity; +import org.opencdmp.commons.types.actionconfirmation.UserInviteToTenantRequestEntity; import org.opencdmp.convention.ConventionService; import org.opencdmp.data.ActionConfirmationEntity; import org.opencdmp.data.TenantEntityManager; @@ -29,6 +30,7 @@ import org.opencdmp.model.actionconfirmation.ActionConfirmation; import org.opencdmp.model.builder.actionconfirmation.ActionConfirmationBuilder; import org.opencdmp.model.deleter.ActionConfirmationDeleter; import org.opencdmp.model.persist.ActionConfirmationPersist; +import org.opencdmp.model.persist.UserInviteToTenantRequestPersist; import org.opencdmp.model.persist.actionconfirmation.DmpInvitationPersist; import org.opencdmp.model.persist.actionconfirmation.MergeAccountConfirmationPersist; import org.opencdmp.model.persist.actionconfirmation.RemoveCredentialRequestPersist; @@ -104,6 +106,7 @@ public class ActionConfirmationServiceImpl implements ActionConfirmationService case MergeAccount -> data.setData(this.xmlHandlingService.toXmlSafe(this.buildMergeAccountConfirmationEntity(model.getMergeAccountConfirmation()))); case DmpInvitation -> data.setData(this.xmlHandlingService.toXmlSafe(this.buildDmpInvitationEntity(model.getDmpInvitation()))); case RemoveCredential -> data.setData(this.xmlHandlingService.toXmlSafe(this.buildMergeAccountConfirmationEntity(model.getRemoveCredentialRequest()))); + case UserInviteToTenant -> data.setData(this.xmlHandlingService.toXmlSafe(this.buildUserInviteToTenantRequestEntity(model.getUserInviteToTenantRequest()))); default -> throw new InternalError("unknown type: " + model.getType()); } data.setUpdatedAt(Instant.now()); @@ -145,6 +148,17 @@ public class ActionConfirmationServiceImpl implements ActionConfirmationService return data; } + private @NotNull UserInviteToTenantRequestEntity buildUserInviteToTenantRequestEntity(UserInviteToTenantRequestPersist persist){ + UserInviteToTenantRequestEntity data = new UserInviteToTenantRequestEntity(); + if (persist == null) return data; + + data.setEmail(persist.getEmail()); + data.setTenantCode(persist.getTenantCode()); + data.setRoles(persist.getRoles()); + + return data; + } + public void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException { logger.debug("deleting : {}", id); diff --git a/backend/core/src/main/java/org/opencdmp/service/user/UserService.java b/backend/core/src/main/java/org/opencdmp/service/user/UserService.java index 8661de670..12c1ac5b9 100644 --- a/backend/core/src/main/java/org/opencdmp/service/user/UserService.java +++ b/backend/core/src/main/java/org/opencdmp/service/user/UserService.java @@ -7,9 +7,7 @@ import gr.cite.tools.exception.MyNotFoundException; import gr.cite.tools.exception.MyValidationException; import gr.cite.tools.fieldset.FieldSet; import jakarta.xml.bind.JAXBException; -import org.opencdmp.model.persist.UserMergeRequestPersist; -import org.opencdmp.model.persist.UserPersist; -import org.opencdmp.model.persist.UserRolePatchPersist; +import org.opencdmp.model.persist.*; import org.opencdmp.model.persist.actionconfirmation.RemoveCredentialRequestPersist; import org.opencdmp.model.user.User; @@ -37,9 +35,13 @@ public interface UserService { void sendRemoveCredentialConfirmation(RemoveCredentialRequestPersist model) throws InvalidApplicationException, JAXBException; + void sendUserToTenantInvitation(UserTenantUsersInviteRequest users) throws InvalidApplicationException, JAXBException; + boolean doesTokenBelongToLoggedInUser(String token) throws InvalidApplicationException, IOException; void confirmMergeAccount(String token) throws InvalidApplicationException, IOException; void confirmRemoveCredential(String token) throws InvalidApplicationException; + + void confirmUserInviteToTenant(String token) throws InvalidApplicationException; } diff --git a/backend/core/src/main/java/org/opencdmp/service/user/UserServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/user/UserServiceImpl.java index 65978f6e3..0c91c9ac3 100644 --- a/backend/core/src/main/java/org/opencdmp/service/user/UserServiceImpl.java +++ b/backend/core/src/main/java/org/opencdmp/service/user/UserServiceImpl.java @@ -33,9 +33,8 @@ import org.opencdmp.commons.scope.tenant.TenantScope; import org.opencdmp.commons.scope.user.UserScope; import org.opencdmp.commons.types.actionconfirmation.MergeAccountConfirmationEntity; import org.opencdmp.commons.types.actionconfirmation.RemoveCredentialRequestEntity; -import org.opencdmp.commons.types.notification.DataType; -import org.opencdmp.commons.types.notification.FieldInfo; -import org.opencdmp.commons.types.notification.NotificationFieldData; +import org.opencdmp.commons.types.actionconfirmation.UserInviteToTenantRequestEntity; +import org.opencdmp.commons.types.notification.*; import org.opencdmp.commons.types.reference.DefinitionEntity; import org.opencdmp.commons.types.user.AdditionalInfoEntity; import org.opencdmp.commons.types.usercredential.UserCredentialDataEntity; @@ -912,4 +911,145 @@ public class UserServiceImpl implements UserService { throw new MyApplicationException("Token has expired!"); } } + + public void sendUserToTenantInvitation(UserTenantUsersInviteRequest users) throws InvalidApplicationException, JAXBException { + String tenantName = null; + String tenantCode = null; + if (this.tenantScope.getTenantCode() != null && !this.tenantScope.getTenantCode().equals(this.tenantScope.getDefaultTenantCode())) { + TenantEntity tenantEntity = this.queryFactory.query(TenantQuery.class).disableTracking().authorize(AuthorizationFlags.AllExceptPublic).codes(this.tenantScope.getTenantCode()).isActive(IsActive.Active).first(); + if (tenantEntity == null) throw new MyApplicationException("Tenant not found"); + tenantName = tenantEntity.getName(); + tenantCode = tenantEntity.getCode(); + } else { + tenantName = "OpenCDMP"; + tenantCode = this.tenantScope.getDefaultTenantCode(); + } + for (UserInviteToTenantRequestPersist user: users.getUsers()) { + String token = this.createUserInviteToTenantConfirmation(user, tenantCode); + this.createTenantSpecificInvitationUserNotificationEvent(token, user.getEmail(), tenantName); + } + + } + + private String createUserInviteToTenantConfirmation(UserInviteToTenantRequestPersist model, String tenantCode) throws JAXBException, InvalidApplicationException { + ActionConfirmationPersist persist = new ActionConfirmationPersist(); + persist.setType(ActionConfirmationType.UserInviteToTenant); + persist.setStatus(ActionConfirmationStatus.Requested); + persist.setToken(UUID.randomUUID().toString()); + persist.setUserInviteToTenantRequest(new UserInviteToTenantRequestPersist()); + persist.getUserInviteToTenantRequest().setEmail(model.getEmail()); + persist.getUserInviteToTenantRequest().setRoles(model.getRoles()); + persist.getUserInviteToTenantRequest().setTenantCode(tenantCode); + persist.setExpiresAt(Instant.now().plusSeconds(this.notificationProperties.getEmailExpirationTimeSeconds())); + this.validatorFactory.validator(ActionConfirmationPersist.ActionConfirmationPersistValidator.class).validateForce(persist); + this.actionConfirmationService.persist(persist, null); + + try { + this.entityManager.disableTenantFilters(); + } finally { + this.entityManager.reloadTenantFilters(); + } + return persist.getToken(); + } + + private void createTenantSpecificInvitationUserNotificationEvent(String token, String email, String tenantName) throws InvalidApplicationException { + UserEntity currentUser = this.entityManager.find(UserEntity.class, this.userScope.getUserIdSafe()); + if (currentUser == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{ this.userScope.getUserIdSafe(), User.class.getSimpleName()}, LocaleContextHolder.getLocale())); + + NotifyIntegrationEvent event = new NotifyIntegrationEvent(); + + List contactPairs = new ArrayList<>(); + contactPairs.add(new ContactPair(ContactInfoType.Email, email)); + + NotificationContactData contactData = new NotificationContactData(contactPairs, null, null); + event.setContactHint(this.jsonHandlingService.toJsonSafe(contactData)); + event.setContactTypeHint(NotificationContactType.EMAIL); + + event.setNotificationType(this.notificationProperties.getTenantSpecificInvitationUserType()); + NotificationFieldData data = new NotificationFieldData(); + List fieldInfoList = new ArrayList<>(); + fieldInfoList.add(new FieldInfo("{userName}", DataType.String, currentUser.getName())); + fieldInfoList.add(new FieldInfo("{confirmationToken}", DataType.String, token)); + fieldInfoList.add(new FieldInfo("{expiration_time}", DataType.String, this.secondsToTime(this.notificationProperties.getEmailExpirationTimeSeconds()))); + fieldInfoList.add(new FieldInfo("{tenantName}", DataType.String, tenantName)); + + data.setFields(fieldInfoList); + event.setData(this.jsonHandlingService.toJsonSafe(data)); + this.eventHandler.handle(event); + } + + public void confirmUserInviteToTenant(String token) throws InvalidApplicationException { + ActionConfirmationEntity action; + try { + this.entityManager.disableTenantFilters(); + action = this.queryFactory.query(ActionConfirmationQuery.class).tokens(token).types(ActionConfirmationType.UserInviteToTenant).isActive(IsActive.Active).first(); + } finally { + this.entityManager.reloadTenantFilters(); + } + if (action == null) + throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{token, ActionConfirmationEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); + this.checkActionState(action); + + UserInviteToTenantRequestEntity userInviteToTenantRequest = this.xmlHandlingService.fromXmlSafe(UserInviteToTenantRequestEntity.class, action.getData()); + if (userInviteToTenantRequest == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{action.getId(), UserInviteToTenantRequestEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); + + TenantEntity tenantEntity = this.queryFactory.query(TenantQuery.class).disableTracking().authorize(AuthorizationFlags.AllExceptPublic).codes(userInviteToTenantRequest.getTenantCode()).isActive(IsActive.Active).first(); + if (tenantEntity == null) throw new MyApplicationException("Tenant not found"); + this.addUserToTenant(tenantEntity, userInviteToTenantRequest); + + } + + private void addUserToTenant(TenantEntity tenant, UserInviteToTenantRequestEntity userInviteToTenantRequest) throws InvalidApplicationException { + + UUID userId = null; + try { + this.entityManager.disableTenantFilters(); + + UserContactInfoEntity contactInfoEntity = this.queryFactory.query(UserContactInfoQuery.class).disableTracking().values(userInviteToTenantRequest.getEmail()).types(ContactInfoType.Email).first(); + if (contactInfoEntity != null){ + userId = contactInfoEntity.getUserId(); + } + + if (userId != null) { + UserCredentialEntity userCredential = this.queryFactory.query(UserCredentialQuery.class).disableTracking().userIds(userId).first(); + if (userCredential == null) throw new MyApplicationException(); + + TenantUserEntity tenantUserEntity = new TenantUserEntity(); + tenantUserEntity.setId(UUID.randomUUID()); + tenantUserEntity.setUserId(userId); + tenantUserEntity.setIsActive(IsActive.Active); + tenantUserEntity.setTenantId(tenant.getId()); + tenantUserEntity.setCreatedAt(Instant.now()); + tenantUserEntity.setUpdatedAt(Instant.now()); + this.entityManager.persist(tenantUserEntity); + this.eventBroker.emit(new UserAddedToTenantEvent(tenantUserEntity.getUserId(), tenantUserEntity.getTenantId())); + + for (String role: userInviteToTenantRequest.getRoles()) { + UserRoleEntity item = new UserRoleEntity(); + item.setId(UUID.randomUUID()); + item.setUserId(userId); + item.setTenantId(tenant.getId()); + item.setRole(role); + item.setCreatedAt(Instant.now()); + this.entityManager.persist(item); + } + this.eventBroker.emit(new UserCredentialTouchedEvent(userCredential.getId(), userCredential.getExternalId())); + + this.entityManager.flush(); + + this.userTouchedIntegrationEventHandler.handle(userId); + this.eventBroker.emit(new UserTouchedEvent(userId)); + + this.entityManager.flush(); + + for (String role: userInviteToTenantRequest.getRoles()) { + this.keycloakService.addUserToTenantRoleGroup(userCredential.getExternalId(), tenant.getCode(), role); + } + } + + } finally { + this.entityManager.reloadTenantFilters(); + } + } + } \ No newline at end of file diff --git a/backend/web/src/main/java/org/opencdmp/controllers/UserController.java b/backend/web/src/main/java/org/opencdmp/controllers/UserController.java index 0068f976b..0a39eddcc 100644 --- a/backend/web/src/main/java/org/opencdmp/controllers/UserController.java +++ b/backend/web/src/main/java/org/opencdmp/controllers/UserController.java @@ -24,9 +24,7 @@ import org.opencdmp.model.builder.DmpAssociatedUserBuilder; import org.opencdmp.model.builder.UserBuilder; import org.opencdmp.model.censorship.DmpAssociatedUserCensor; import org.opencdmp.model.censorship.UserCensor; -import org.opencdmp.model.persist.UserMergeRequestPersist; -import org.opencdmp.model.persist.UserPersist; -import org.opencdmp.model.persist.UserRolePatchPersist; +import org.opencdmp.model.persist.*; import org.opencdmp.model.persist.actionconfirmation.RemoveCredentialRequestPersist; import org.opencdmp.model.result.QueryResult; import org.opencdmp.model.user.User; @@ -334,4 +332,33 @@ public class UserController { return true; } + + @PostMapping("invite-users-to-tenant") + @Transactional + @ValidationFilterAnnotation(validator = UserTenantUsersInviteRequest.UserTenantUsersInviteRequestValidator.ValidatorName, argumentName = "model") + public Boolean inviteUsersToTenant(@RequestBody UserTenantUsersInviteRequest users) throws InvalidApplicationException, JAXBException { + logger.debug(new MapLogEntry("send tenant invitation to users").And("users", users)); + + this.userTypeService.sendUserToTenantInvitation(users); + + this.auditService.track(AuditableAction.User_InviteToTenant, Map.ofEntries( + new AbstractMap.SimpleEntry("users", users) + )); + + return true; + } + + @GetMapping("confirm-invite-user-to-tenant/token/{token}") + @Transactional + public Boolean confirmInviteUserToTenant(@PathVariable("token") String token) throws InvalidApplicationException { + logger.debug(new MapLogEntry("confirm merge account to user").And("token", token)); + + this.userTypeService.confirmUserInviteToTenant(token); + + this.auditService.track(AuditableAction.User_InviteToTenantConfirm, Map.ofEntries( + new AbstractMap.SimpleEntry("token", token) + )); + + return true; + } } diff --git a/backend/web/src/main/resources/config/notification-devel.yml b/backend/web/src/main/resources/config/notification-devel.yml index eeb865325..fd2e7f976 100644 --- a/backend/web/src/main/resources/config/notification-devel.yml +++ b/backend/web/src/main/resources/config/notification-devel.yml @@ -13,4 +13,5 @@ notification: descriptionTemplateInvitationType: 223BB607-EFA1-4CE7-99EC-4BEABFEF9A8B contactSupportType: 5B1D6C52-88F9-418B-9B8A-6F1F963D9EAD publicContactSupportType: B542B606-ACC6-4629-ADEF-4D8EE2F01222 + tenantSpecificInvitationUserType: 497dada5-eccc-4bc0-9e0b-63e22b4eb0be contactSupportEmail: support@dmp.com \ No newline at end of file diff --git a/dmp-frontend/src/app/ui/dmp/dmp-editor-blueprint/dmp-editor.component.html b/dmp-frontend/src/app/ui/dmp/dmp-editor-blueprint/dmp-editor.component.html index 8949d4b31..891d80f76 100644 --- a/dmp-frontend/src/app/ui/dmp/dmp-editor-blueprint/dmp-editor.component.html +++ b/dmp-frontend/src/app/ui/dmp/dmp-editor-blueprint/dmp-editor.component.html @@ -57,8 +57,8 @@ -
- +
+
diff --git a/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/DescriptionTemplateXmlMigrationService.java b/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/DescriptionTemplateXmlMigrationService.java index 1568873aa..de94e8d66 100644 --- a/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/DescriptionTemplateXmlMigrationService.java +++ b/dmp-migration-tool/web/src/main/java/eu/old/eudat/migration/DescriptionTemplateXmlMigrationService.java @@ -660,15 +660,18 @@ public class DescriptionTemplateXmlMigrationService { ExternalFetcherApiSourceConfigurationEntity apiEntity = new ExternalFetcherApiSourceConfigurationEntity(); + + String source = persist.getAutoCompleteOptions() != null ? persist.getAutoCompleteOptions().getSource() : null; + + if ( source == null || source.isEmpty()) { URI uri; if (persist.getUrl().contains("?")) { uri = new URI(persist.getUrl().substring(0, persist.getUrl().trim().lastIndexOf("?"))); } else { uri = new URI(persist.getUrl().trim()); } - String source = persist.getAutoCompleteOptions().getSource(); - source = source != null && !source.isEmpty() ? source : uri.getHost(); - + source = uri.getHost(); + } String parsedUrl = persist.getUrl().trim(); parsedUrl = parsedUrl.replace("%20", " "); parsedUrl = parsedUrl.replace("%22", "\""); @@ -705,14 +708,14 @@ public class DescriptionTemplateXmlMigrationService { data.setResultsArrayPath(persist.getOptionsRoot()); - if (persist.getAutoCompleteOptions() == null && this.conventionService.isNullOrEmpty(persist.getAutoCompleteOptions().getLabel())) { + if (persist.getAutoCompleteOptions() != null && !this.conventionService.isNullOrEmpty(persist.getAutoCompleteOptions().getLabel())) { data.setFieldsMapping(new ArrayList<>()); ResultFieldsMappingConfigurationEntity labelField = new ResultFieldsMappingConfigurationEntity(); labelField.setCode(ReferenceEntity.KnownFields.Label); labelField.setResponsePath(persist.getAutoCompleteOptions().getLabel()); data.getFieldsMapping().add(labelField); } - if (persist.getAutoCompleteOptions() == null && this.conventionService.isNullOrEmpty(persist.getAutoCompleteOptions().getValue())) { + if (persist.getAutoCompleteOptions() != null && !this.conventionService.isNullOrEmpty(persist.getAutoCompleteOptions().getValue())) { ResultFieldsMappingConfigurationEntity idField = new ResultFieldsMappingConfigurationEntity(); idField.setCode(ReferenceEntity.KnownFields.ReferenceId); idField.setResponsePath(persist.getAutoCompleteOptions().getValue()); diff --git a/notification-service/notification-web/src/main/resources/config/notification-devel.yml b/notification-service/notification-web/src/main/resources/config/notification-devel.yml index 5c3907265..279b00eaf 100644 --- a/notification-service/notification-web/src/main/resources/config/notification-devel.yml +++ b/notification-service/notification-web/src/main/resources/config/notification-devel.yml @@ -48,6 +48,9 @@ notification: - #publicContactSupportType type: B542B606-ACC6-4629-ADEF-4D8EE2F01222 contacts: [ email ] + - #tenantSpecificInvitationUserType + type: 497dada5-eccc-4bc0-9e0b-63e22b4eb0be + contacts: [ email ] message: email: flows: @@ -386,6 +389,29 @@ notification: bcc-mode: 0 allow-attachments: false cipher-fields: [ ] + - #tenantSpecificInvitationUserType + key: 497dada5-eccc-4bc0-9e0b-63e22b4eb0be + subject-path: classpath:notification_templates/tenantspecificinvitationuser/email/subject.{language}.txt + subject-field-options: + mandatory: [ ] + optional: [ ] + body-path: classpath:notification_templates/tenantspecificinvitationuser/email/body.{language}.html + body-field-options: + mandatory: [ "{userName}", "{installation-url}", "{confirmationToken}", "{tenantName}" ] + optional: + - key: "{expiration_time}" + value: --- + formatting: + '[{userName}]': null + '[{tenantName}]': null + '[{installation-url}]': null + '[{expiration_time}]': null + cc: [ ] + cc-mode: 0 + bcc: [ ] + bcc-mode: 0 + allow-attachments: false + cipher-fields: [ ] template-cache: prefix: ${CACHE_DISAMBIGUATION:} key-pattern: "{prefix}:Notification_Message_Email_Template:{key}:v0" diff --git a/notification-service/notification-web/src/main/resources/notification_templates/tenantspecificinvitationuser/email/body.en.html b/notification-service/notification-web/src/main/resources/notification_templates/tenantspecificinvitationuser/email/body.en.html new file mode 100644 index 000000000..a5ff85526 --- /dev/null +++ b/notification-service/notification-web/src/main/resources/notification_templates/tenantspecificinvitationuser/email/body.en.html @@ -0,0 +1,304 @@ + + + + + + OpenCDMP Notification + + + + + + + + + +
  +
+ + + This is preheader text. Some clients will show this text as a preview. + + + + + + + + +
+ + + + +
+ OpenCDMP +

User {userName} invited you to join the {tenantName}.

+

Please confirm that you want to continue. +
The link will expire in {expiration_time}.

+ + + + + + +
+ + + + + + +
Confirm Tenant Invitation
+
+
+
+ + + + + + +
+
 
+ + \ No newline at end of file diff --git a/notification-service/notification-web/src/main/resources/notification_templates/tenantspecificinvitationuser/email/subject.en.txt b/notification-service/notification-web/src/main/resources/notification_templates/tenantspecificinvitationuser/email/subject.en.txt new file mode 100644 index 000000000..af89e2391 --- /dev/null +++ b/notification-service/notification-web/src/main/resources/notification_templates/tenantspecificinvitationuser/email/subject.en.txt @@ -0,0 +1 @@ +OpenCDMP - Tenant Invitation \ No newline at end of file