Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plugin - Alert Messenger - Implement (I4I submission) #694

Open
wants to merge 22 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
50b7839
Plugin - Alert Messenger - Now on Quarkus 3.13.3
GregJohnStewart Aug 23, 2024
04f1c0a
First commit
BrendanAndrews Sep 23, 2024
7434721
First successful test of mock email sending
BrendanAndrews Sep 23, 2024
1787108
Fixing email address
BrendanAndrews Sep 23, 2024
e20f850
Adding SMTP Server configuration and real email testing (even though …
BrendanAndrews Sep 24, 2024
b6211a9
Adding some additional comments for easier reading
BrendanAndrews Sep 25, 2024
4d5c4ab
Pushing Slack function and test code (commented out test code)
BrendanAndrews Sep 26, 2024
7ea4180
Fixing some issue with getting information from the token
BrendanAndrews Nov 15, 2024
ac01608
Database connection works
BrendanAndrews Dec 2, 2024
ac38968
Fixing my credentials that I accidentally pushed with new changed ones
BrendanAndrews Dec 2, 2024
ca3fc17
Fixing environment variables
BrendanAndrews Dec 2, 2024
1db77b2
Storing slack webhook url and whether the user wants to receive email…
BrendanAndrews Dec 4, 2024
860a208
Created AlertConsumer.java business logic for sending messages.
BrendanAndrews Dec 4, 2024
b8b69fe
I got the AlertConsumer.java file working in a mock test.
BrendanAndrews Dec 4, 2024
71ca97c
Version of the Alert Messenger that's fully functional
BrendanAndrews Dec 4, 2024
74f47a9
Removing unnecessary commented out code
BrendanAndrews Dec 4, 2024
cb5d101
Removing ExampleConsumer and TestConsumerTest
BrendanAndrews Dec 6, 2024
0253c4c
Added TEST_USER_ID environment variable for automated Slack message t…
BrendanAndrews Dec 6, 2024
9d973cb
Adding SMTP_USERNAME environment variable to allow other SMTP servers…
BrendanAndrews Dec 6, 2024
d82fcb2
Added comments to my code
BrendanAndrews Dec 6, 2024
c4280e4
Merge branch 'dev' into dev.677-fr-alert-messenger-plugin-implement-i…
GregJohnStewart Dec 8, 2024
3223898
Merge branch 'dev' into dev.677-fr-alert-messenger-plugin-implement-i…
GregJohnStewart Dec 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions software/plugins/alert-messenger/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,25 @@ dependencies {
implementation 'io.quarkus:quarkus-config-yaml'
implementation 'io.quarkus:quarkus-oidc'
implementation 'io.quarkus:quarkus-qute'
implementation 'io.quarkus:quarkus-mailer'
implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'

// Added dependencies for database ORM and JWT for enhanced security and user management
implementation 'io.quarkus:quarkus-hibernate-orm-panache'
implementation 'io.quarkus:quarkus-jdbc-postgresql'
implementation 'io.quarkus:quarkus-smallrye-jwt'

// Add Quarkus REST Client dependency
implementation 'io.quarkus:quarkus-rest-client'

implementation 'tech.ebp.oqm.lib:core-api-lib-quarkus:2.2.0-SNAPSHOT'
implementation 'org.jboss.slf4j:slf4j-jboss-logmanager'

testImplementation 'io.quarkus:quarkus-junit5'
testImplementation 'io.rest-assured:rest-assured'

// Added Mockito for mocking in unit tests.
testImplementation 'org.mockito:mockito-core:5.4.0'

}

group 'tech.ebp.openQuarterMaster'
Expand All @@ -46,4 +59,4 @@ compileJava {

compileTestJava {
options.encoding = 'UTF-8'
}
}
6 changes: 3 additions & 3 deletions software/plugins/alert-messenger/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Gradle properties
quarkusPluginId=io.quarkus
quarkusPluginVersion=3.13.2
quarkusPluginVersion=3.13.3
quarkusPlatformVersion=3.13.3
quarkusPlatformGroupId=io.quarkus.platform
quarkusPlatformArtifactId=quarkus-bom
quarkusPlatformVersion=3.13.2
quarkusPlatformArtifactId=quarkus-bom
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package tech.ebp.oqm.plugin.alertMessenger;

import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.microprofile.reactive.messaging.Incoming;
import org.eclipse.microprofile.reactive.messaging.Message;
import tech.ebp.oqm.plugin.alertMessenger.model.UserPreferences;
import tech.ebp.oqm.plugin.alertMessenger.repositories.UserPreferencesRepository;
import tech.ebp.oqm.plugin.alertMessenger.repositories.UserRepository;
import tech.ebp.oqm.plugin.alertMessenger.utils.EmailUtils;
import tech.ebp.oqm.plugin.alertMessenger.utils.SlackUtils;

import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletionStage;


// AlertConsumer processes Kafka messages from the `oqm-core-all-events` topic
// and sends alerts via email and Slack based on user preferences.
@Slf4j
@ApplicationScoped
public class AlertConsumer {

@Inject
private UserPreferencesRepository userPreferencesRepository;

@Inject
private UserRepository userRepository;

@Inject
private EmailUtils email;

@Inject
private SlackUtils slack;

// Initializes the AlertConsumer bean and logs startup information.
@PostConstruct
public void init() {
log.info("Starting AlertConsumer");
}

// Handles incoming Kafka messages, processes their payload,
// and routes alerts based on user preferences.
@Incoming("oqm-core-all-events")
public CompletionStage<Void> receive(Message<ObjectNode> message) {
try {
ObjectNode payload = message.getPayload();
log.info("Received alert message: {}", payload);

processMessage(payload);

return message.ack();
} catch (Exception e) {
log.error("Error processing message: {}", e.getMessage(), e);
return message.nack(e);
}
}

// Validates the payload structure, retrieves user preferences,
// and decides whether to send email or Slack notifications.
private void processMessage(ObjectNode payload) {
if (!payload.has("type") || !payload.has("details") || !payload.has("userId")) {
log.error("Invalid payload: {}", payload);
return;
}

String alertType = payload.get("type").asText();
String alertDetails = payload.get("details").asText();
UUID userId = UUID.fromString(payload.get("userId").asText());

Optional<UserPreferences> preferencesOptional = userPreferencesRepository.findByIdOptional(userId);
if (preferencesOptional.isEmpty()) {
log.warn("No user preferences found for user ID: {}", userId);
return;
}

UserPreferences preferences = preferencesOptional.get();

if ("yes".equalsIgnoreCase(preferences.getEmailNotifications())) {
handleEmailNotification(userId, alertType, alertDetails);
} else {
log.info("Email notifications not enabled for user ID: {}", userId);
}

if (preferences.getSlackWebhook() != null && !preferences.getSlackWebhook().isEmpty()) {
handleSlackNotification(userId, alertType, alertDetails);
} else {
log.info("Slack notifications not enabled for user ID: {}", userId);
}
}

// Sends email notifications using the EmailUtils class if the user's email is configured.
private void handleEmailNotification(UUID userId, String alertType, String alertDetails) {
var userInfo = userRepository.findById(userId);
if (userInfo == null || userInfo.getEmail() == null) {
log.warn("No email found for user ID: {}", userId);
return;
}
email.sendEmail(userInfo.getEmail(), alertType, alertDetails);
log.info("Email sent to user ID {}: {}", userId, userInfo.getEmail());
}

// Sends Slack notifications using the SlackUtils class if the user has a Slack webhook configured.
private void handleSlackNotification(UUID userId, String alertType, String alertDetails) {
slack.sendSlackMessage(userId, String.format("%s: \n%s", alertType, alertDetails));
log.info("Slack message sent to user ID {}", userId);
}

// Setter methods for testing
public void setUserPreferencesRepository(UserPreferencesRepository userPreferencesRepository) {
this.userPreferencesRepository = userPreferencesRepository;
}

public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}

public void setEmailUtils(EmailUtils email) {
this.email = email;
}

public void setSlackUtils(SlackUtils slack) {
this.slack = slack;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,52 @@

import io.quarkus.qute.Location;
import io.quarkus.qute.Template;
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.transaction.Transactional;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import tech.ebp.oqm.plugin.alertMessenger.repositories.UserPreferencesRepository;
import tech.ebp.oqm.plugin.alertMessenger.repositories.UserRepository;
import tech.ebp.oqm.plugin.alertMessenger.AlertConsumer;
import tech.ebp.oqm.plugin.alertMessenger.model.UserInfo;
import tech.ebp.oqm.plugin.alertMessenger.model.UserPreferences;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import io.quarkus.hibernate.orm.panache.PanacheRepository;

import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

import org.eclipse.microprofile.jwt.JsonWebToken;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.eclipse.microprofile.openapi.annotations.tags.Tags;

import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

/*
* Remove this after testing
*/
import org.eclipse.microprofile.reactive.messaging.Message;

@Slf4j
@Path("/")
@Tags({@Tag(name = "UI")})
@Tags({ @Tag(name = "UI") })
@RequestScoped
@Produces(MediaType.TEXT_HTML)
public class Home extends UiInterface {
Expand All @@ -28,14 +57,107 @@ public class Home extends UiInterface {
@Location("webui/pages/index")
Template pageTemplate;

@Inject
JsonWebToken jwt; // Extracts claims from the JWT

@Inject
UserRepository userRepository; // Repository for database operations

@Inject
UserPreferencesRepository userPreferencesRepository;


@Inject
SecurityIdentity identity; // Provides user and role info


// Ensures the user is saved/updated in the database based on JWT claims
// and renders the home page with the user's information.
@GET
@RolesAllowed("inventoryView")
@Produces(MediaType.TEXT_HTML)
@Transactional // Ensures a transaction is active
public Response index() {
log.info("Got index page");

// Extract user details from the JWT
String idString = jwt.getClaim("sub"); // Extract the ID as a string
UUID id;
try {
id = UUID.fromString(idString); // Convert String to UUID
} catch (IllegalArgumentException e) {
log.error("Invalid UUID format in 'sub' claim: {}", idString);
return Response.status(Response.Status.BAD_REQUEST).entity("Invalid user ID format").build();
}

String name = jwt.getClaim("name");
String username = jwt.getClaim("preferred_username");
String email = jwt.getClaim("email");
Set<String> roles = identity.getRoles(); // Retrieves user roles

// Ensure the user is saved or updated in the database
UserInfo existingUser = userRepository.findById(id);
if (existingUser != null) {
existingUser.setName(name);
existingUser.setUsername(username);
existingUser.setEmail(email);
existingUser.getRoles().clear(); // Clear existing roles
existingUser.getRoles().addAll(roles); // Add new roles
// No need to merge; Hibernate automatically tracks changes to managed entities.
} else {
log.info("New user detected, saving to database: {}", username);
UserInfo newUser = UserInfo.builder()
.id(id)
.name(name)
.username(username)
.email(email)
.roles(roles)
.build();
userRepository.persist(newUser);
}

// Render the UI page

return Response.ok(
this.setupPageTemplate(this.pageTemplate)
).build();
this.setupPageTemplate(this.pageTemplate)).build();
}

// Updates user notification preferences (email and Slack webhook) in the database.
@POST
@Path("/updatePreferences")
@PermitAll
@Transactional
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response updatePreferences(@FormParam("id") String idString,
@FormParam("emailNotifications") String emailNotifications,
@FormParam("slackWebhook") String slackWebhook) {

log.info("Received data: id={}, emailNotifications={}, slackWebhook={}", idString, emailNotifications, slackWebhook);

UUID id;
try {
id = UUID.fromString(idString);
} catch (IllegalArgumentException e) {
log.error("Invalid UUID format for ID: {}", idString);
return Response.status(Response.Status.BAD_REQUEST).entity("Invalid ID format").build();
}

// Retrieve or create the preferences record
UserPreferences preferences = userPreferencesRepository.findByIdOptional(id).orElseGet(() -> {
log.info("Creating new preferences record for user: {}", id);
UserPreferences newPreferences = new UserPreferences();
newPreferences.setId(id);
userPreferencesRepository.persist(newPreferences);
return newPreferences;
});

// Update preferences
preferences.setEmailNotifications(emailNotifications);
preferences.setSlackWebhook(slackWebhook);

// Persist updated preferences
userPreferencesRepository.persist(preferences);

return Response.ok("Preferences updated successfully!").build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ protected String getBearerHeaderStr() {

@PostConstruct
void initialLogAndEntityProcess() {
if (this.getUserToken() == null) {
log.warn("JWT token is null. Skipping user initialization.");
return;
}

this.oqmDatabases = this.oqmDatabaseService.getDatabases();
this.userInfo = JwtUtils.getUserInfo(this.getUserToken());

Expand All @@ -99,7 +104,6 @@ public String getSelectedDb() {
if(this.oqmDatabases == null || this.oqmDatabases.isEmpty()){
throw new IllegalStateException("Cannot have no databases.");
}
//TODO: this but smarter?
return this.getOqmDatabases().get(0).get("id").asText();
}
return this.oqmDb;
Expand Down
Loading
Loading