argos/dmp-backend/core/src/main/java/eu/eudat/service/user/UserServiceImpl.java

462 lines
23 KiB
Java

package eu.eudat.service.user;
import com.fasterxml.jackson.core.JsonProcessingException;
import eu.eudat.authorization.AuthorizationFlags;
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.user.UserScope;
import eu.eudat.commons.types.actionconfirmation.EmailConfirmationEntity;
import eu.eudat.commons.types.notification.*;
import eu.eudat.commons.types.user.AdditionalInfoEntity;
import eu.eudat.commons.validation.ValidatorFactory;
import eu.eudat.configurations.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.NotificationIntegrationEvent;
import eu.eudat.integrationevent.outbox.notification.NotificationIntegrationEventHandler;
import eu.eudat.model.User;
import eu.eudat.model.UserContactInfo;
import eu.eudat.model.builder.UserBuilder;
import eu.eudat.model.deleter.UserCredentialDeleter;
import eu.eudat.model.deleter.UserDeleter;
import eu.eudat.model.deleter.UserRoleDeleter;
import eu.eudat.model.persist.*;
import eu.eudat.model.persist.actionconfirmation.EmailConfirmationPersist;
import eu.eudat.query.*;
import eu.eudat.service.actionconfirmation.ActionConfirmationService;
import eu.eudat.service.keycloak.KeycloakRole;
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.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 jakarta.persistence.EntityManager;
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 EntityManager 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 NotificationIntegrationEventHandler eventHandler;
private final ValidatorFactory validatorFactory;
@Autowired
public UserServiceImpl(
EntityManager 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, NotificationIntegrationEventHandler eventHandler, ValidatorFactory validatorFactory) {
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;
}
//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()));
return this.builderFactory.builder(UserBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).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));
}
//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<User> 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<UserCredentialEntity> 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");
UUID subjectId = UUID.fromString(userCredentials.getFirst().getExternalId());
List<UserRoleEntity> existingItems = this.queryFactory.query(UserRoleQuery.class).userIds(data.getId()).collect();
List<UUID> foundIds = new ArrayList<>();
for (String roleName : model.getRoles().stream().filter(x-> x != null && !x.isBlank()).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(data.getId());
item.setRole(roleName);
item.setCreatedAt(Instant.now());
this.entityManager.persist(item);
this.keycloakService.addUserToGroup(subjectId, KeycloakRole.valueOf(roleName));
}
foundIds.add(item.getId());
}
this.entityManager.flush();
List<UserRoleEntity> toDelete = existingItems.stream().filter(x-> foundIds.stream().noneMatch(y-> y.equals(x.getId()))).collect(Collectors.toList());
toDelete.forEach(x -> this.keycloakService.removeUserFromGroup(subjectId, KeycloakRole.valueOf(x.getRole())));
this.deleterFactory.deleter(UserRoleDeleter.class).deleteAndSave(toDelete);
this.entityManager.flush();
this.eventBroker.emit(new UserTouchedEvent(data.getId()));
return this.builderFactory.builder(UserBuilder.class).authorize(AuthorizationFlags.OwnerOrDmpAssociatedOrPermissionOrPublic).build(BaseFieldSet.build(fields, User._id), data);
}
//region mine
@Override
public void updateLanguageMine(String language) throws JsonProcessingException {
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.eventBroker.emit(new UserTouchedEvent(data.getId()));
}
@Override
public void updateTimezoneMine(String timezone) throws JsonProcessingException {
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.eventBroker.emit(new UserTouchedEvent(data.getId()));
}
@Override
public void updateCultureMine(String culture) throws JsonProcessingException {
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.eventBroker.emit(new UserTouchedEvent(data.getId()));
}
//endregion
//notifications
public void sendMergeAccountConfirmation(String email) throws InvalidApplicationException, JAXBException {
String token = this.createActionConfirmation(email, ActionConfirmationType.MergeAccount);
NotificationIntegrationEvent event = new NotificationIntegrationEvent();
event.setUserId(this.userScope.getUserIdSafe());
List<ContactPair> contactPairs = new ArrayList<>();
contactPairs.add(new ContactPair(ContactInfoType.Email, email));
NotificationContactData contactData = new NotificationContactData(contactPairs, null, null);
event.setContactHint(jsonHandlingService.toJsonSafe(contactData));
event.setContactTypeHint(NotificationContactType.EMAIL);
event.setNotificationType(notificationProperties.getMergeAccountConfirmationType());
NotificationFieldData data = new NotificationFieldData();
List<FieldInfo> fieldInfoList = new ArrayList<>();
fieldInfoList.add(new FieldInfo("{userName}", DataType.String, this.queryFactory.query(UserQuery.class).ids(this.userScope.getUserIdSafe()).first().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(String email) throws InvalidApplicationException, JAXBException {
UserContactInfoEntity userContactInfo = this.queryFactory.query(UserContactInfoQuery.class).types(ContactInfoType.Email).userIds(this.userScope.getUserIdSafe()).values(email).first();
if(userContactInfo == null){
throw new MyApplicationException("Email does not exist in this user!");
}
UserCredentialQuery query = this.queryFactory.query(UserCredentialQuery.class).userIds(this.userScope.getUserIdSafe());
if (query == null || query.count() == 0){
throw new MyApplicationException("This user don't have credential!");
}
String token = this.createActionConfirmation(email, ActionConfirmationType.RemoveCredential);
NotificationIntegrationEvent event = new NotificationIntegrationEvent();
event.setUserId(this.userScope.getUserIdSafe());
List<ContactPair> contactPairs = new ArrayList<>();
contactPairs.add(new ContactPair(ContactInfoType.Email, email));
NotificationContactData contactData = new NotificationContactData(contactPairs, null, null);
event.setContactHint(jsonHandlingService.toJsonSafe(contactData));
event.setContactTypeHint(NotificationContactType.EMAIL);
event.setNotificationType(notificationProperties.getRemoveCredentialConfirmationType());
NotificationFieldData data = new NotificationFieldData();
List<FieldInfo> fieldInfoList = new ArrayList<>();
fieldInfoList.add(new FieldInfo("{confirmationToken}", DataType.String, token));
fieldInfoList.add(new FieldInfo("{email}", DataType.String, email));
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 createActionConfirmation(String email, ActionConfirmationType type) throws JAXBException, InvalidApplicationException {
ActionConfirmationPersist persist = new ActionConfirmationPersist();
persist.setType(type);
persist.setStatus(ActionConfirmationStatus.Requested);
persist.setToken(UUID.randomUUID().toString());
persist.setEmailConfirmation(new EmailConfirmationPersist(email));
persist.setCreatedById(this.userScope.getUserId());
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 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);
EmailConfirmationEntity emailConfirmation = this.xmlHandlingService.fromXmlSafe(EmailConfirmationEntity.class, action.getData());
action.setStatus(ActionConfirmationStatus.Accepted);
//TODO merge
// this.entityManager.merge(action);
}
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);
UserCredentialEntity userCredential = this.queryFactory.query(UserCredentialQuery.class).userIds(this.userScope.getUserIdSafe()).first();
if (userCredential == null){
throw new MyApplicationException("This user does not have credential");
}
this.deleterFactory.deleter(UserCredentialDeleter.class).deleteAndSaveByIds(List.of(userCredential.getId()));
action.setStatus(ActionConfirmationStatus.Accepted);
this.entityManager.merge(action);
}
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!");
}
}
}