Skip to content

Commit

Permalink
Notify the user via email about granted project access (#365)
Browse files Browse the repository at this point in the history
After a user has been granted access to a project, they will now get
notified with an email.

The email also contains a link to the project to simplify access for the user.
  • Loading branch information
sven1103 authored Sep 11, 2023
1 parent d7e9d1c commit 40b7fa7
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package life.qbic.authorization.application;

/**
* <b>App Context Provider</b>
* <p>
* Provides some utility methods to create navigation targets within the application.
*
* @since 1.0.0
*/
public interface AppContextProvider {

/**
* Returns a resolvable URL to the target project resource in the application.
*
* @param projectId the project id as the target web resource
* @return a fully resolvable URL
* @since 1.0.0
*/
String urlToProject(String projectId);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package life.qbic.authorization.application.policy;

import java.util.Objects;
import life.qbic.authorization.application.policy.directive.InformUserAboutGrantedAccess;
import life.qbic.domain.concepts.DomainEventDispatcher;

/**
* <b>Policy: Project access granted</b>
* <p>
* Business policy that needs to be executed after a project access has been granted.
*
* @since 1.0.0
*/
public class ProjectAccessGrantedPolicy {

private InformUserAboutGrantedAccess informUserAboutGrantedAccess;

public ProjectAccessGrantedPolicy(InformUserAboutGrantedAccess informUserAboutGrantedAccess) {
this.informUserAboutGrantedAccess = Objects.requireNonNull(informUserAboutGrantedAccess);
DomainEventDispatcher.instance().subscribe(informUserAboutGrantedAccess);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package life.qbic.authorization.application.policy.directive;

import java.util.Objects;
import life.qbic.authentication.domain.user.concept.User;
import life.qbic.authentication.domain.user.concept.UserId;
import life.qbic.authentication.domain.user.repository.UserRepository;
import life.qbic.authorization.application.AppContextProvider;
import life.qbic.domain.concepts.DomainEvent;
import life.qbic.domain.concepts.DomainEventSubscriber;
import life.qbic.domain.concepts.communication.EmailService;
import life.qbic.projectmanagement.domain.project.service.event.ProjectAccessGranted;
import org.jobrunr.jobs.annotations.Job;
import org.jobrunr.scheduling.JobScheduler;
import org.springframework.stereotype.Component;

/**
* <b>Directive: Inform user about granted access</b>
* <p>
* Notifies the user via email about the recently granted project access.
*
* @since 1.0.0
*/
@Component
public class InformUserAboutGrantedAccess implements DomainEventSubscriber<ProjectAccessGranted> {

private final EmailService emailService;

private final JobScheduler jobScheduler;
private final UserRepository userRepository;

private final AppContextProvider appContextProvider;

public InformUserAboutGrantedAccess(EmailService emailService, JobScheduler jobScheduler,
UserRepository userRepository, AppContextProvider appContextProvider) {
this.emailService = Objects.requireNonNull(emailService);
this.jobScheduler = Objects.requireNonNull(jobScheduler);
this.userRepository = Objects.requireNonNull(userRepository);
this.appContextProvider = Objects.requireNonNull(appContextProvider);
}

private String composeMessage(String projectId, User recipient, String projectTitle) {
return String.format("""
Dear %s,
you have been granted access to project:
'%s'
Please click the link below to access the project after login:
%s
Need help? Contact us for further questions at [email protected]
Best regards,\
The QBiC team
""", recipient.fullName(), projectTitle, appContextProvider.urlToProject(projectId));
}

@Override
public Class<? extends DomainEvent> subscribedToEventType() {
return ProjectAccessGranted.class;
}

@Override
public void handleEvent(ProjectAccessGranted event) {
jobScheduler.enqueue(() -> notifyUser(event.forUserId(), event.forProjectId(), event.forProjectTitle()));
}

@Job(name = "Notify user about granted project access")
public void notifyUser(String userId, String projectId, String projectTitle)
throws RuntimeException {
var recipient = userRepository.findById(UserId.from(userId)).get();
emailService.send(recipient.emailAddress().get(), recipient.fullName().get(),
"Project access granted", composeMessage(projectId, recipient, projectTitle));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
public interface EmailService {

void send(Email email);

void send(String recipient, String recipientFullName, String subject, String message);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import life.qbic.application.commons.ApplicationResponse;
import life.qbic.domain.concepts.communication.Email;
import life.qbic.domain.concepts.communication.EmailService;
import life.qbic.domain.concepts.communication.Recipient;
import life.qbic.logging.api.Logger;
import life.qbic.logging.service.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -78,6 +79,16 @@ public void send(Email email) {
}
}

@Override
public void send(String recipientAddress, String recipientFullName, String subject, String message) {
var email = new Email(message, subject, "[email protected]",
new Recipient(recipientAddress, recipientFullName), "text/plain");
sendPlainEmail(email).ifSuccessOrElse(
successResponse -> reportSuccess(successResponse, email),
failureResponse -> reportFailure(failureResponse, email)
);
}

private void reportSuccess(ApplicationResponse applicationResponse, Email email) {
String emailSendSuccessMessage = String.format(
"Email with subject '%s' successfully send to '%s'",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package life.qbic.projectmanagement.domain.project.service;

import java.util.Objects;
import life.qbic.domain.concepts.DomainEventDispatcher;
import life.qbic.projectmanagement.domain.project.ProjectId;
import life.qbic.projectmanagement.domain.project.repository.ProjectRepository;
import life.qbic.projectmanagement.domain.project.service.event.ProjectAccessGranted;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
* <b>Access Domain Service</b>
* <p>
* Service that will emit domain events for project access related tasks, that are not part of
* domain aggregates.
*
* @since 1.0.0
*/
@Service
public class AccessDomainService {

private final ProjectRepository projectRepository;

@Autowired
public AccessDomainService(ProjectRepository projectRepository) {
this.projectRepository = Objects.requireNonNull(projectRepository);
}

/**
* Inform the domain service, that a user has been granted with access for a certain project.
*
* @param projectId the project id of the affected project
* @param userId the user that has been granted with access for the project
* @since 1.0.0
*/
public void grantProjectAccessFor(String projectId, String userId) {
var projectTitle = projectRepository.find(ProjectId.parse(projectId)).get().getProjectIntent()
.projectTitle().title();
var projectAccessGranted = ProjectAccessGranted.create(userId, projectId, projectTitle);
DomainEventDispatcher.instance().dispatch(projectAccessGranted);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package life.qbic.projectmanagement.domain.project.service.event;

import java.io.Serial;
import java.time.Instant;
import java.util.Objects;
import life.qbic.domain.concepts.DomainEvent;

/**
* <b>Project Access Granted Event</b>
*
* <p>This event is emitted after access has been granted to a user</p>
*
* @since 1.0.0
*/
public class ProjectAccessGranted extends DomainEvent {

@Serial
private static final long serialVersionUID = 199678646014632540L;
private final String userId;
private final String projectId;

private final String projectTitle;

private final Instant occurredOn;

private ProjectAccessGranted(Instant occurredOn, String userId, String projectId,
String projectTitle) {
this.userId = Objects.requireNonNull(userId);
this.projectId = Objects.requireNonNull(projectId);
this.projectTitle = Objects.requireNonNull(projectTitle);
this.occurredOn = occurredOn;
}

public static ProjectAccessGranted create(String userId, String projectId, String projectTitle) {
return new ProjectAccessGranted(Instant.now(), userId, projectId, projectTitle);
}

@Override
public Instant occurredOn() {
return this.occurredOn;
}

public String forUserId() {
return userId;
}

public String forProjectId() {
return projectId;
}

public String forProjectTitle() {
return projectTitle;
}
}
13 changes: 13 additions & 0 deletions vaadinfrontend/src/main/java/life/qbic/datamanager/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@
import life.qbic.authentication.application.user.registration.UserRegistrationService;
import life.qbic.authentication.domain.user.repository.UserDataStorage;
import life.qbic.authentication.domain.user.repository.UserRepository;
import life.qbic.authorization.application.AppContextProvider;
import life.qbic.authorization.application.policy.ProjectAccessGrantedPolicy;
import life.qbic.authorization.application.policy.directive.InformUserAboutGrantedAccess;
import life.qbic.broadcasting.Exchange;
import life.qbic.broadcasting.MessageBusSubmission;
import life.qbic.domain.concepts.SimpleEventStore;
import life.qbic.domain.concepts.TemporaryEventRepository;
import life.qbic.domain.concepts.communication.EmailService;
import life.qbic.projectmanagement.application.api.SampleCodeService;
import life.qbic.projectmanagement.application.batch.BatchRegistrationService;
import life.qbic.projectmanagement.application.policy.ProjectRegisteredPolicy;
Expand Down Expand Up @@ -117,4 +121,13 @@ public ProjectRegisteredPolicy projectRegisteredPolicy(SampleCodeService sampleC
projectRepository);
return new ProjectRegisteredPolicy(createNewSampleStatisticsEntry);
}

@Bean
public ProjectAccessGrantedPolicy projectAccessGrantedPolicy(EmailService emailService,
JobScheduler jobScheduler, UserRepository userRepository,
AppContextProvider appContextProvider) {
var informUserAboutGrantedAccess = new InformUserAboutGrantedAccess(emailService, jobScheduler,
userRepository, appContextProvider);
return new ProjectAccessGrantedPolicy(informUserAboutGrantedAccess);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package life.qbic.datamanager;

import java.net.MalformedURLException;
import java.net.URL;
import life.qbic.authorization.application.AppContextProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
* <b>Data Manager context provider</b>
* <p>
* Simple implementation of the {@link AppContextProvider} interface.
*
* @since 1.0.0
*/
@Component
public class DataManagerContextProvider implements AppContextProvider {

private final String protocol;
private final String host;
private final int port;
private final String context;
private final String endpoint;

public DataManagerContextProvider(
@Value("${service.host.protocol}") String protocol,
@Value("${service.host.name}") String host,
@Value("${service.host.port}") int port,
@Value("${server.servlet.context-path}") String contextPath,
@Value("${project-endpoint}") String projectEndpoint) {
this.protocol = protocol;
this.host = host;
this.port = port;
this.context = contextPath;
this.endpoint = projectEndpoint;
}

@Override
public String urlToProject(String projectId) {
var fullPath = context + endpoint + "/" + projectId;
try {
return new URL(protocol, host, port, fullPath).toExternalForm();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import life.qbic.datamanager.views.general.PageArea;
import life.qbic.logging.api.Logger;
import life.qbic.projectmanagement.domain.project.ProjectId;
import life.qbic.projectmanagement.domain.project.service.AccessDomainService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.core.userdetails.UserDetailsService;
Expand Down Expand Up @@ -53,17 +54,21 @@ public class ProjectAccessComponent extends PageArea implements BeforeEnterObser
private final Grid<RoleProjectAccess> roleProjectAccessGrid = new Grid<>(RoleProjectAccess.class);
private ProjectId projectId;

private final AccessDomainService accessDomainService;

protected ProjectAccessComponent(
@Autowired ProjectAccessService projectAccessService,
@Autowired UserDetailsService userDetailsService,
@Autowired UserRepository userRepository,
@Autowired SidRepository sidRepository,
@Autowired UserPermissions userPermissions) {
@Autowired UserPermissions userPermissions,
@Autowired AccessDomainService accessDomainService) {
this.projectAccessService = projectAccessService;
this.userDetailsService = userDetailsService;
this.userRepository = userRepository;
this.sidRepository = sidRepository;
this.userPermissions = userPermissions;
this.accessDomainService = accessDomainService;
requireNonNull(projectAccessService, "projectAccessService must not be null");
requireNonNull(userDetailsService, "userDetailsService must not be null");
requireNonNull(userRepository, "userRepository must not be null");
Expand Down Expand Up @@ -231,6 +236,7 @@ private void addEditUserAccessToProjectDialogListeners(
private void addUsersToProject(List<User> users) {
for (User user : users) {
projectAccessService.grant(user.emailAddress().get(), projectId, BasePermission.READ);
accessDomainService.grantProjectAccessFor(projectId.value(), user.id().get());
}
}

Expand Down
3 changes: 3 additions & 0 deletions vaadinfrontend/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ email-confirmation-parameter=${EMAIL_CONFIRMATION_PARAMETER:confirm-email}
# route for password reset
password-reset-endpoint=${PASSWORD_RESET_ENDPOINT:/registration/new-password}
password-reset-parameter=${PASSWORD_RESET_PARAMETER:user-id}
# route to project resource
project-endpoint=${PROJECT_ENDPOINT:/projects}

# openbis-client credentials
openbis.user.name=${OPENBIS_USER_NAME:openbis-username}
openbis.user.password=${OPENBIS_USER_PASSWORD:openbis-password}
Expand Down

0 comments on commit 40b7fa7

Please sign in to comment.