package eu.eudat.service.user; import com.fasterxml.jackson.core.JsonProcessingException; import eu.eudat.authorization.AuthorizationFlags; import eu.eudat.authorization.AuthorizationProperties; import eu.eudat.authorization.OwnedResource; import eu.eudat.authorization.Permission; import eu.eudat.commons.JsonHandlingService; import eu.eudat.commons.XmlHandlingService; import eu.eudat.commons.enums.ActionConfirmationStatus; import eu.eudat.commons.enums.ActionConfirmationType; import eu.eudat.commons.enums.ContactInfoType; import eu.eudat.commons.enums.IsActive; import eu.eudat.commons.enums.notification.NotificationContactType; import eu.eudat.commons.scope.tenant.TenantScope; import eu.eudat.commons.scope.user.UserScope; import eu.eudat.commons.types.actionconfirmation.MergeAccountConfirmationEntity; import eu.eudat.commons.types.actionconfirmation.RemoveCredentialRequestEntity; import eu.eudat.commons.types.notification.*; import eu.eudat.commons.types.user.AdditionalInfoEntity; import eu.eudat.commons.types.usercredential.UserCredentialDataEntity; import eu.eudat.commons.notification.NotificationProperties; import eu.eudat.convention.ConventionService; import eu.eudat.data.*; import eu.eudat.errorcode.ErrorThesaurusProperties; import eu.eudat.event.UserTouchedEvent; import eu.eudat.event.EventBroker; import eu.eudat.integrationevent.outbox.notification.NotifyIntegrationEvent; import eu.eudat.integrationevent.outbox.notification.NotifyIntegrationEventHandler; import eu.eudat.integrationevent.outbox.userremoval.UserRemovalIntegrationEventHandler; import eu.eudat.integrationevent.outbox.usertouched.UserTouchedIntegrationEventHandler; import eu.eudat.model.User; import eu.eudat.model.UserContactInfo; import eu.eudat.model.UserCredential; import eu.eudat.model.builder.UserBuilder; import eu.eudat.model.deleter.*; import eu.eudat.model.persist.*; import eu.eudat.model.persist.actionconfirmation.MergeAccountConfirmationPersist; import eu.eudat.model.persist.actionconfirmation.RemoveCredentialRequestPersist; import eu.eudat.query.*; import eu.eudat.service.actionconfirmation.ActionConfirmationService; import eu.eudat.service.elastic.ElasticService; import eu.eudat.service.keycloak.KeycloakService; import gr.cite.commons.web.authz.service.AuthorizationService; import gr.cite.tools.data.builder.BuilderFactory; import gr.cite.tools.data.deleter.DeleterFactory; import gr.cite.tools.data.query.Ordering; import gr.cite.tools.data.query.QueryFactory; import gr.cite.tools.exception.MyApplicationException; import gr.cite.tools.exception.MyForbiddenException; import gr.cite.tools.exception.MyNotFoundException; import gr.cite.tools.exception.MyValidationException; import gr.cite.tools.fieldset.BaseFieldSet; import gr.cite.tools.fieldset.FieldSet; import gr.cite.tools.logging.LoggerService; import gr.cite.tools.logging.MapLogEntry; import gr.cite.tools.validation.ValidatorFactory; import jakarta.xml.bind.JAXBException; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; import org.apache.commons.csv.QuoteMode; import org.jetbrains.annotations.NotNull; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.stereotype.Service; import javax.management.InvalidApplicationException; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.time.Instant; import java.util.*; import java.util.stream.Collectors; @Service public class UserServiceImpl implements UserService { private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(UserServiceImpl.class)); private final TenantEntityManager entityManager; private final AuthorizationService authorizationService; private final DeleterFactory deleterFactory; private final BuilderFactory builderFactory; private final ConventionService conventionService; private final ErrorThesaurusProperties errors; private final MessageSource messageSource; private final EventBroker eventBroker; private final JsonHandlingService jsonHandlingService; private final XmlHandlingService xmlHandlingService; private final QueryFactory queryFactory; private final UserScope userScope; private final KeycloakService keycloakService; private final ActionConfirmationService actionConfirmationService; private final NotificationProperties notificationProperties; private final NotifyIntegrationEventHandler eventHandler; private final ValidatorFactory validatorFactory; private final ElasticService elasticService; private final UserTouchedIntegrationEventHandler userTouchedIntegrationEventHandler; private final UserRemovalIntegrationEventHandler userRemovalIntegrationEventHandler; private final AuthorizationProperties authorizationProperties; private final TenantScope tenantScope; @Autowired public UserServiceImpl( TenantEntityManager entityManager, AuthorizationService authorizationService, DeleterFactory deleterFactory, BuilderFactory builderFactory, ConventionService conventionService, ErrorThesaurusProperties errors, MessageSource messageSource, EventBroker eventBroker, JsonHandlingService jsonHandlingService, XmlHandlingService xmlHandlingService, QueryFactory queryFactory, UserScope userScope, KeycloakService keycloakService, ActionConfirmationService actionConfirmationService, NotificationProperties notificationProperties, NotifyIntegrationEventHandler eventHandler, ValidatorFactory validatorFactory, ElasticService elasticService, UserTouchedIntegrationEventHandler userTouchedIntegrationEventHandler, UserRemovalIntegrationEventHandler userRemovalIntegrationEventHandler, AuthorizationProperties authorizationProperties, TenantScope tenantScope) { this.entityManager = entityManager; this.authorizationService = authorizationService; this.deleterFactory = deleterFactory; this.builderFactory = builderFactory; this.conventionService = conventionService; this.errors = errors; this.messageSource = messageSource; this.eventBroker = eventBroker; this.jsonHandlingService = jsonHandlingService; this.xmlHandlingService = xmlHandlingService; this.queryFactory = queryFactory; this.userScope = userScope; this.keycloakService = keycloakService; this.actionConfirmationService = actionConfirmationService; this.notificationProperties = notificationProperties; this.eventHandler = eventHandler; this.validatorFactory = validatorFactory; this.elasticService = elasticService; this.userTouchedIntegrationEventHandler = userTouchedIntegrationEventHandler; this.userRemovalIntegrationEventHandler = userRemovalIntegrationEventHandler; this.authorizationProperties = authorizationProperties; this.tenantScope = tenantScope; } //region persist @Override public User persist(UserPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException, JsonProcessingException { logger.debug(new MapLogEntry("persisting data User").And("model", model).And("fields", fields)); this.authorizationService.authorizeAtLeastOneForce(model.getId() != null ? List.of(new OwnedResource(model.getId())) : null, Permission.EditUser); Boolean isUpdate = this.conventionService.isValidGuid(model.getId()); UserEntity data; if (isUpdate) { data = this.entityManager.find(UserEntity.class, model.getId()); if (data == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{model.getId(), User.class.getSimpleName()}, LocaleContextHolder.getLocale())); if (!this.conventionService.hashValue(data.getUpdatedAt()).equals(model.getHash())) throw new MyValidationException(this.errors.getHashConflict().getCode(), this.errors.getHashConflict().getMessage()); } else { data = new UserEntity(); data.setId(UUID.randomUUID()); data.setIsActive(IsActive.Active); data.setCreatedAt(Instant.now()); } data.setAdditionalInfo(this.jsonHandlingService.toJson(this.buildAdditionalInfoEntity(model.getAdditionalInfo()))); data.setName(model.getName()); data.setUpdatedAt(Instant.now()); if (isUpdate) this.entityManager.merge(data); else this.entityManager.persist(data); this.entityManager.flush(); this.eventBroker.emit(new UserTouchedEvent(data.getId())); this.userTouchedIntegrationEventHandler.handle(data.getId()); return this.builderFactory.builder(UserBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).build(BaseFieldSet.build(fields, User._id), data); } private @NotNull AdditionalInfoEntity buildAdditionalInfoEntity(UserAdditionalInfoPersist persist){ AdditionalInfoEntity data = new AdditionalInfoEntity(); if (persist == null) return data; data.setOrganizationId(persist.getOrganizationId()); data.setRoleOrganization(persist.getRoleOrganization()); data.setCulture(persist.getCulture()); data.setTimezone(persist.getTimezone()); data.setLanguage(persist.getLanguage()); data.setAvatarUrl(persist.getAvatarUrl()); return data; } //endregion //region delete @Override public void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException { logger.debug("deleting User: {}", id); this.authorizationService.authorizeForce(Permission.DeleteUser); this.deleterFactory.deleter(UserDeleter.class).deleteAndSaveByIds(List.of(id)); this.userRemovalIntegrationEventHandler.handle(id); } //endregion //region export @Override public byte[] exportCsv() throws IOException { this.authorizationService.authorizeForce(Permission.ExportUsers); FieldSet fieldSet = new BaseFieldSet().ensure(User._id).ensure(User._name).ensure(User._contacts + "." + UserContactInfo._value).ensure(User._contacts + "." + UserContactInfo._type); List users = this.builderFactory.builder(UserBuilder.class).build(fieldSet, this.queryFactory.query(UserQuery.class).collectAs(fieldSet)); final ByteArrayOutputStream out = new ByteArrayOutputStream(); final CSVFormat format = CSVFormat.DEFAULT.withHeader("User Id", "User Name", "User Email").withQuoteMode(QuoteMode.NON_NUMERIC); final CSVPrinter csvPrinter = new CSVPrinter(new PrintWriter(out), format); for (User user : users) { csvPrinter.printRecord(user.getId(), user.getName(), (user.getContacts() != null ? String.join(" ", user.getContacts().stream().filter(x-> ContactInfoType.Email.equals(x.getType())).map(UserContactInfo::getValue).toList()) : "")); } csvPrinter.flush(); return out.toByteArray(); } //endregion @Override public User patchRoles(UserRolePatchPersist model, FieldSet fields) throws InvalidApplicationException { logger.debug(new MapLogEntry("persisting data UserRole").And("model", model).And("fields", fields)); this.authorizationService.authorizeForce(Permission.EditUser); UserEntity data = this.entityManager.find(UserEntity.class, model.getId()); if (data == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{model.getId(), User.class.getSimpleName()}, LocaleContextHolder.getLocale())); if (!this.conventionService.hashValue(data.getUpdatedAt()).equals(model.getHash())) throw new MyValidationException(this.errors.getHashConflict().getCode(), this.errors.getHashConflict().getMessage()); List userCredentials = this.queryFactory.query(UserCredentialQuery.class).userIds(data.getId()).collect(); if (userCredentials.isEmpty()) throw new MyApplicationException("Currently cannot update roles for this user"); if (userCredentials.getFirst().getExternalId() == null) throw new MyApplicationException("Currently cannot update roles for this user"); String subjectId = userCredentials.getFirst().getExternalId(); this.applyGlobalRoles(data.getId(), subjectId, model); this.applyTenantRoles(data.getId(), subjectId, model); this.entityManager.flush(); this.eventBroker.emit(new UserTouchedEvent(data.getId())); this.userTouchedIntegrationEventHandler.handle(data.getId()); return this.builderFactory.builder(UserBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermission).build(BaseFieldSet.build(fields, User._id), data); } private void applyGlobalRoles(UUID userId, String subjectId, UserRolePatchPersist model) throws InvalidApplicationException { try { this.tenantScope.setTempTenant(this.entityManager.getEntityManager(), null, this.tenantScope.getDefaultTenantCode()); List existingItems = this.queryFactory.query(UserRoleQuery.class).userIds(userId).tenantIsSet(false).roles(this.authorizationProperties.getAllowedGlobalRoles()).collect(); List foundIds = new ArrayList<>(); for (String roleName : model.getRoles().stream().filter(x -> x != null && !x.isBlank() && this.authorizationProperties.getAllowedGlobalRoles().contains(x)).distinct().toList()) { UserRoleEntity item = existingItems.stream().filter(x -> x.getRole().equals(roleName)).findFirst().orElse(null); if (item == null) { item = new UserRoleEntity(); item.setId(UUID.randomUUID()); item.setUserId(userId); item.setRole(roleName); item.setCreatedAt(Instant.now()); this.entityManager.persist(item); this.keycloakService.addUserToGlobalRoleGroup(subjectId, roleName); } foundIds.add(item.getId()); } this.entityManager.flush(); List toDelete = existingItems.stream().filter(x -> foundIds.stream().noneMatch(y -> y.equals(x.getId()))).collect(Collectors.toList()); toDelete.forEach(x -> this.keycloakService.removeUserGlobalRoleGroup(subjectId, x.getRole())); this.deleterFactory.deleter(UserRoleDeleter.class).deleteAndSave(toDelete); this.entityManager.flush(); } finally { this.tenantScope.removeTempTenant(this.entityManager.getEntityManager()); } } private void applyTenantRoles(UUID userId, String subjectId, UserRolePatchPersist model) throws InvalidApplicationException { if (!tenantScope.isSet()) throw new MyForbiddenException("tenant scope required"); UserRoleQuery userRoleQuery = this.queryFactory.query(UserRoleQuery.class).userIds(userId).roles(this.authorizationProperties.getAllowedTenantRoles()); if (tenantScope.isDefaultTenant()) userRoleQuery.tenantIsSet(false); else userRoleQuery.tenantIsSet(true).tenantIds(this.tenantScope.getTenant()); List existingItems = userRoleQuery.collect(); List foundIds = new ArrayList<>(); for (String roleName : model.getRoles().stream().filter(x-> x != null && !x.isBlank() && this.authorizationProperties.getAllowedTenantRoles().contains(x)).distinct().toList()) { UserRoleEntity item = existingItems.stream().filter(x-> x.getRole().equals(roleName)).findFirst().orElse(null); if (item == null) { item = new UserRoleEntity(); item.setId(UUID.randomUUID()); item.setUserId(userId); item.setRole(roleName); item.setCreatedAt(Instant.now()); item.setTenantId(this.tenantScope.getTenant()); this.entityManager.persist(item); this.keycloakService.addUserToTenantRoleGroup(subjectId, this.tenantScope.getTenantCode(), roleName); } foundIds.add(item.getId()); } this.entityManager.flush(); List toDelete = existingItems.stream().filter(x-> foundIds.stream().noneMatch(y-> y.equals(x.getId()))).collect(Collectors.toList()); toDelete.forEach(x -> { try { this.keycloakService.removeUserTenantRoleGroup(subjectId, this.tenantScope.getTenantCode(), x.getRole()); } catch (InvalidApplicationException e) { throw new RuntimeException(e); } }); this.deleterFactory.deleter(UserRoleDeleter.class).deleteAndSave(toDelete); this.entityManager.flush(); } //region mine @Override public void updateLanguageMine(String language) throws JsonProcessingException, InvalidApplicationException { logger.debug(new MapLogEntry("persisting User language").And("language", language)); UUID userId = this.userScope.getUserIdSafe(); if (userId == null) throw new MyForbiddenException(this.errors.getForbidden().getCode(), this.errors.getForbidden().getMessage()); UserEntity data = this.entityManager.find(UserEntity.class, userId); if (data == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{userId, User.class.getSimpleName()}, LocaleContextHolder.getLocale())); AdditionalInfoEntity additionalInfoEntity = this.jsonHandlingService.fromJsonSafe(AdditionalInfoEntity.class, data.getAdditionalInfo()); if (additionalInfoEntity == null) additionalInfoEntity = new AdditionalInfoEntity(); additionalInfoEntity.setLanguage(language); data.setAdditionalInfo(this.jsonHandlingService.toJson(additionalInfoEntity)); data.setUpdatedAt(Instant.now()); this.entityManager.merge(data); this.entityManager.flush(); this.userTouchedIntegrationEventHandler.handle(data.getId()); this.eventBroker.emit(new UserTouchedEvent(data.getId())); } @Override public void updateTimezoneMine(String timezone) throws JsonProcessingException, InvalidApplicationException { logger.debug(new MapLogEntry("persisting User timezone").And("timezone", timezone)); UUID userId = this.userScope.getUserIdSafe(); if (userId == null) throw new MyForbiddenException(this.errors.getForbidden().getCode(), this.errors.getForbidden().getMessage()); UserEntity data = this.entityManager.find(UserEntity.class, userId); if (data == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{userId, User.class.getSimpleName()}, LocaleContextHolder.getLocale())); AdditionalInfoEntity additionalInfoEntity = this.jsonHandlingService.fromJsonSafe(AdditionalInfoEntity.class, data.getAdditionalInfo()); if (additionalInfoEntity == null) additionalInfoEntity = new AdditionalInfoEntity(); additionalInfoEntity.setTimezone(timezone); data.setAdditionalInfo(this.jsonHandlingService.toJson(additionalInfoEntity)); data.setUpdatedAt(Instant.now()); this.entityManager.merge(data); this.entityManager.flush(); this.userTouchedIntegrationEventHandler.handle(data.getId()); this.eventBroker.emit(new UserTouchedEvent(data.getId())); } @Override public void updateCultureMine(String culture) throws JsonProcessingException, InvalidApplicationException { logger.debug(new MapLogEntry("persisting User culture").And("culture", culture)); UUID userId = this.userScope.getUserIdSafe(); if (userId == null) throw new MyForbiddenException(this.errors.getForbidden().getCode(), this.errors.getForbidden().getMessage()); UserEntity data = this.entityManager.find(UserEntity.class, userId); if (data == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{userId, User.class.getSimpleName()}, LocaleContextHolder.getLocale())); AdditionalInfoEntity additionalInfoEntity = this.jsonHandlingService.fromJsonSafe(AdditionalInfoEntity.class, data.getAdditionalInfo()); if (additionalInfoEntity == null) additionalInfoEntity = new AdditionalInfoEntity(); additionalInfoEntity.setCulture(culture); data.setAdditionalInfo(this.jsonHandlingService.toJson(additionalInfoEntity)); data.setUpdatedAt(Instant.now()); this.entityManager.merge(data); this.entityManager.flush(); this.userTouchedIntegrationEventHandler.handle(data.getId()); this.eventBroker.emit(new UserTouchedEvent(data.getId())); } //endregion //notifications public void sendMergeAccountConfirmation(UserMergeRequestPersist model) throws InvalidApplicationException, JAXBException { UserContactInfoEntity userContactInfoEntity = this.queryFactory.query(UserContactInfoQuery.class).values(model.getEmail()).types(ContactInfoType.Email).first(); if (userContactInfoEntity == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{model.getEmail(), User.class.getSimpleName()}, LocaleContextHolder.getLocale())); UserEntity user = this.queryFactory.query(UserQuery.class).ids(userContactInfoEntity.getUserId()).isActive(IsActive.Active).first(); if (user == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{userContactInfoEntity.getUserId(), User.class.getSimpleName()}, LocaleContextHolder.getLocale())); String token = this.createMergeAccountConfirmation(model.getEmail()); createMergeNotificationEvent(token, user, model.getEmail(), NotificationContactType.EMAIL); } private void createMergeNotificationEvent(String token, UserEntity user, String email, NotificationContactType type) throws InvalidApplicationException { NotifyIntegrationEvent event = new NotifyIntegrationEvent(); event.setUserId(user.getId()); List contactPairs = new ArrayList<>(); contactPairs.add(new ContactPair(ContactInfoType.Email, email)); NotificationContactData contactData = new NotificationContactData(contactPairs, null, null); event.setContactHint(jsonHandlingService.toJsonSafe(contactData)); event.setContactTypeHint(type); event.setNotificationType(notificationProperties.getMergeAccountConfirmationType()); NotificationFieldData data = new NotificationFieldData(); List fieldInfoList = new ArrayList<>(); fieldInfoList.add(new FieldInfo("{userName}", DataType.String, user.getName())); fieldInfoList.add(new FieldInfo("{confirmationToken}", DataType.String, token)); fieldInfoList.add(new FieldInfo("{expiration_time}", DataType.String, this.secondsToTime(this.notificationProperties.getEmailExpirationTimeSeconds()))); data.setFields(fieldInfoList); event.setData(jsonHandlingService.toJsonSafe(data)); eventHandler.handle(event); } public void sendRemoveCredentialConfirmation(RemoveCredentialRequestPersist model) throws InvalidApplicationException, JAXBException { UserCredentialEntity data = this.entityManager.find(UserCredentialEntity.class, model.getCredentialId()); if (data == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{model.getCredentialId(), UserCredentialEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); if (!data.getUserId().equals(this.userScope.getUserId())) throw new MyForbiddenException(this.errors.getForbidden().getCode(), this.errors.getForbidden().getMessage()); String token = this.createRemoveConfirmation(data.getId()); this.createRemoveCredentialNotificationEvent(token, data.getUserId(), NotificationContactType.EMAIL); } private void createRemoveCredentialNotificationEvent(String token, UUID userId, NotificationContactType type) throws InvalidApplicationException { NotifyIntegrationEvent event = new NotifyIntegrationEvent(); event.setUserId(userId); event.setContactTypeHint(type); event.setNotificationType(notificationProperties.getRemoveCredentialConfirmationType()); NotificationFieldData data = new NotificationFieldData(); List fieldInfoList = new ArrayList<>(); fieldInfoList.add(new FieldInfo("{confirmationToken}", DataType.String, token)); fieldInfoList.add(new FieldInfo("{expiration_time}", DataType.String, this.secondsToTime(this.notificationProperties.getEmailExpirationTimeSeconds()))); data.setFields(fieldInfoList); event.setData(jsonHandlingService.toJsonSafe(data)); eventHandler.handle(event); } private String createMergeAccountConfirmation(String email) throws JAXBException, InvalidApplicationException { ActionConfirmationPersist persist = new ActionConfirmationPersist(); persist.setType(ActionConfirmationType.MergeAccount); persist.setStatus(ActionConfirmationStatus.Requested); persist.setToken(UUID.randomUUID().toString()); persist.setMergeAccountConfirmation(new MergeAccountConfirmationPersist()); persist.getMergeAccountConfirmation().setEmail(email); persist.setExpiresAt(Instant.now().plusSeconds(this.notificationProperties.getEmailExpirationTimeSeconds())); validatorFactory.validator(ActionConfirmationPersist.ActionConfirmationPersistValidator.class).validateForce(persist); this.actionConfirmationService.persist(persist, null); return persist.getToken(); } private String createRemoveConfirmation(UUID credentialId) throws JAXBException, InvalidApplicationException { ActionConfirmationPersist persist = new ActionConfirmationPersist(); persist.setType(ActionConfirmationType.RemoveCredential); persist.setStatus(ActionConfirmationStatus.Requested); persist.setToken(UUID.randomUUID().toString()); persist.setRemoveCredentialRequest(new RemoveCredentialRequestPersist()); persist.getRemoveCredentialRequest().setCredentialId(credentialId); persist.setExpiresAt(Instant.now().plusSeconds(this.notificationProperties.getEmailExpirationTimeSeconds())); validatorFactory.validator(ActionConfirmationPersist.ActionConfirmationPersistValidator.class).validateForce(persist); this.actionConfirmationService.persist(persist, null); return persist.getToken(); } private String secondsToTime(int seconds) { int sec = seconds % 60; int hour = seconds / 60; int min = hour % 60; hour = hour / 60; return (hour + ":" + min + ":" + sec); } public void confirmMergeAccount(String token) throws IOException, InvalidApplicationException { ActionConfirmationEntity action = this.queryFactory.query(ActionConfirmationQuery.class).tokens(token).types(ActionConfirmationType.MergeAccount).isActive(IsActive.Active).first(); if (action == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{token, ActionConfirmationEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); this.checkActionState(action); MergeAccountConfirmationEntity mergeAccountConfirmationEntity = this.xmlHandlingService.fromXmlSafe(MergeAccountConfirmationEntity.class, action.getData()); if (mergeAccountConfirmationEntity == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{mergeAccountConfirmationEntity, MergeAccountConfirmationEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); UserContactInfoEntity userContactInfoEntity = this.queryFactory.query(UserContactInfoQuery.class).values(mergeAccountConfirmationEntity.getEmail()).types(ContactInfoType.Email).first(); if (userContactInfoEntity == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{mergeAccountConfirmationEntity.getEmail(), User.class.getSimpleName()}, LocaleContextHolder.getLocale())); UserEntity userToBeMerge = this.queryFactory.query(UserQuery.class).ids(userContactInfoEntity.getUserId()).isActive(IsActive.Active).first(); if (userToBeMerge == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{userContactInfoEntity.getUserId(), User.class.getSimpleName()}, LocaleContextHolder.getLocale())); if (!this.userScope.getUserIdSafe().equals(userToBeMerge.getId())) throw new MyForbiddenException("Only requested user can approve"); UserEntity newUser = this.queryFactory.query(UserQuery.class).ids(action.getCreatedById()).isActive(IsActive.Active).first(); if (newUser == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{action.getCreatedById(), User.class.getSimpleName()}, LocaleContextHolder.getLocale())); if (!newUser.getId().equals(userToBeMerge.getId())){ this.mergeNewUserToOld(newUser, userToBeMerge); } action.setUpdatedAt(Instant.now()); action.setStatus(ActionConfirmationStatus.Accepted); this.entityManager.merge(action); this.entityManager.flush(); this.userTouchedIntegrationEventHandler.handle(newUser.getId()); this.userRemovalIntegrationEventHandler.handle(userToBeMerge.getId()); } private void mergeNewUserToOld(UserEntity newUser, UserEntity oldUser) throws IOException, InvalidApplicationException { List userCredentials = this.queryFactory.query(UserCredentialQuery.class).userIds(oldUser.getId()).collect(); for (UserCredentialEntity userCredential : userCredentials){ userCredential.setUserId(newUser.getId()); this.entityManager.merge(userCredential); } List userContactInfos = this.queryFactory.query(UserContactInfoQuery.class).userIds(oldUser.getId()).collect(); UserContactInfoQuery newUserContactInfoQuery = this.queryFactory.query(UserContactInfoQuery.class).userIds(newUser.getId()); newUserContactInfoQuery.setOrder(new Ordering().addDescending(UserContactInfo._ordinal)); UserContactInfoEntity newUserContactInfo = newUserContactInfoQuery.first(); int ordinal = newUserContactInfo == null ? 0 : newUserContactInfo.getOrdinal() + 1; for (UserContactInfoEntity userContactInfo : userContactInfos){ userContactInfo.setUserId(newUser.getId()); userContactInfo.setOrdinal(ordinal); this.entityManager.merge(userContactInfo); ordinal++; } List userRoles = this.queryFactory.query(UserRoleQuery.class).userIds(oldUser.getId()).collect(); List newUserRoles = this.queryFactory.query(UserRoleQuery.class).userIds(newUser.getId()).collect(); List rolesToDelete = new ArrayList<>(); for (UserRoleEntity userRole : userRoles){ if (newUserRoles.stream().anyMatch(x-> x.getRole().equals(userRole.getRole()))) { rolesToDelete.add(userRole); } else { userRole.setUserId(newUser.getId()); this.entityManager.merge(userRole); } } this.deleterFactory.deleter(UserRoleDeleter.class).delete(rolesToDelete); List userSettings = this.queryFactory.query(UserSettingsQuery.class).entityIds(oldUser.getId()).collect(); List newUserSettings = this.queryFactory.query(UserSettingsQuery.class).entityIds(newUser.getId()).collect(); List userSettingsToDelete = new ArrayList<>(); for (UserSettingsEntity userSetting : userSettings){ if (newUserSettings.stream().anyMatch(x-> x.getKey().equals(userSetting.getKey()))) { userSettingsToDelete.add(userSetting); } else { userSetting.setEntityId(newUser.getId()); this.entityManager.merge(userSetting); } } this.deleterFactory.deleter(UserSettingsSettingsDeleter.class).delete(userSettingsToDelete); List tags = this.queryFactory.query(TagQuery.class).createdByIds(oldUser.getId()).collect(); for (TagEntity tag : tags){ tag.setCreatedById(newUser.getId()); this.entityManager.merge(tag); } List storageFiles = this.queryFactory.query(StorageFileQuery.class).ownerIds(oldUser.getId()).collect(); for (StorageFileEntity storageFile : storageFiles){ storageFile.setOwnerId(newUser.getId()); this.entityManager.merge(storageFile); } List locks = this.queryFactory.query(LockQuery.class).lockedByIds(oldUser.getId()).collect(); for (LockEntity lock : locks){ lock.setLockedBy(newUser.getId()); this.entityManager.merge(lock); } List dmpUsers = this.queryFactory.query(DmpUserQuery.class).userIds(oldUser.getId()).collect(); for (DmpUserEntity dmpUser : dmpUsers){ dmpUser.setUserId(newUser.getId()); this.entityManager.merge(dmpUser); } List userDescriptionTemplates = this.queryFactory.query(UserDescriptionTemplateQuery.class).userIds(oldUser.getId()).collect(); for (UserDescriptionTemplateEntity userDescriptionTemplate : userDescriptionTemplates){ userDescriptionTemplate.setUserId(newUser.getId()); this.entityManager.merge(userDescriptionTemplate); } List dmps = this.queryFactory.query(DmpQuery.class).creatorIds(oldUser.getId()).collect(); for (DmpEntity dmp : dmps){ dmp.setCreatorId(newUser.getId()); this.entityManager.merge(dmp); } List descriptions = this.queryFactory.query(DescriptionQuery.class).createdByIds(oldUser.getId()).collect(); for (DescriptionEntity description : descriptions){ description.setCreatedById(newUser.getId()); this.entityManager.merge(description); } oldUser.setIsActive(IsActive.Inactive); this.entityManager.merge(oldUser); this.entityManager.flush(); for (DmpEntity dmp : dmps){ this.elasticService.persistDmp(dmp); } for (DescriptionEntity description : descriptions){ this.elasticService.persistDescription(description); } } public void confirmRemoveCredential(String token) throws InvalidApplicationException { ActionConfirmationEntity action = this.queryFactory.query(ActionConfirmationQuery.class).tokens(token).types(ActionConfirmationType.RemoveCredential).isActive(IsActive.Active).first(); if (action == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{token, ActionConfirmationEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); this.checkActionState(action); RemoveCredentialRequestEntity removeCredentialRequestEntity = this.xmlHandlingService.fromXmlSafe(RemoveCredentialRequestEntity.class, action.getData()); if (removeCredentialRequestEntity == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{removeCredentialRequestEntity, RemoveCredentialRequestEntity.class.getSimpleName()}, LocaleContextHolder.getLocale())); UserCredentialEntity userCredentialEntity = this.queryFactory.query(UserCredentialQuery.class).ids(removeCredentialRequestEntity.getCredentialId()).first(); if (userCredentialEntity == null) throw new MyNotFoundException(messageSource.getMessage("General_ItemNotFound", new Object[]{removeCredentialRequestEntity.getCredentialId(), UserCredential.class.getSimpleName()}, LocaleContextHolder.getLocale())); if (!this.userScope.getUserIdSafe().equals(userCredentialEntity.getId())) throw new MyForbiddenException("Only requested user can approve"); if (userCredentialEntity.getData() != null){ UserCredentialDataEntity userCredentialDataEntity = this.jsonHandlingService.fromJsonSafe(UserCredentialDataEntity.class, userCredentialEntity.getData()); if (userCredentialDataEntity != null && !this.conventionService.isNullOrEmpty(userCredentialDataEntity.getEmail())) { List userContactInfos = this.queryFactory.query(UserContactInfoQuery.class).values(userCredentialDataEntity.getEmail()).userIds(userCredentialEntity.getUserId()).collect(); if (!this.conventionService.isListNullOrEmpty(userContactInfos)) this.deleterFactory.deleter(UserContactInfoDeleter.class).delete(userContactInfos); } } this.deleterFactory.deleter(UserCredentialDeleter.class).delete(List.of(userCredentialEntity)); action.setUpdatedAt(Instant.now()); action.setStatus(ActionConfirmationStatus.Accepted); this.entityManager.merge(action); this.entityManager.flush(); this.userTouchedIntegrationEventHandler.handle(userCredentialEntity.getUserId()); } private void checkActionState(ActionConfirmationEntity action) throws MyApplicationException { if (action.getStatus().equals(ActionConfirmationStatus.Accepted)){ throw new MyApplicationException("Account is already confirmed!"); } if (action.getExpiresAt().compareTo(Instant.now()) < 0){ throw new MyApplicationException("Token has expired!"); } } }