Skip to content

Commit

Permalink
update notification service and add signup confirmation process
Browse files Browse the repository at this point in the history
  • Loading branch information
walimorris committed Aug 1, 2024
1 parent 70be4c0 commit f232253
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(
"/api/auth/**",
"/api/subscriptions/**",
"/api/quiz-generation/**",
"/api/quiz-retrieval/**",
"/login",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.morris.quizly.controllers;

import com.morris.quizly.models.security.AuthRequest;
import com.morris.quizly.models.security.JwtTokenProvider;
import com.morris.quizly.models.security.Roles;
import com.morris.quizly.models.security.SignupRequest;
import com.morris.quizly.models.security.*;
import com.morris.quizly.services.NotificationService;
import com.morris.quizly.services.QuizlyUserDetailsService;
import com.morris.quizly.utils.FileUtil;
import org.slf4j.Logger;
Expand All @@ -17,14 +15,14 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

@RestController
@RequestMapping("/api/auth")
Expand All @@ -35,6 +33,7 @@ public class AuthController {
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final QuizlyUserDetailsService quizlyUserDetailsService;
private final NotificationService notificationService;

private static final String USER_DETAILS = "userDetails";
private static final String ACCESS_TOKEN = "accessToken";
Expand All @@ -53,11 +52,13 @@ public class AuthController {

@Autowired
public AuthController(AuthenticationManager authenticationManager, PasswordEncoder passwordEncoder,
JwtTokenProvider jwtTokenProvider, QuizlyUserDetailsService quizlyDetailsService) {
JwtTokenProvider jwtTokenProvider, QuizlyUserDetailsService quizlyDetailsService,
NotificationService notificationService) {
this.authenticationManager = authenticationManager;
this.passwordEncoder = passwordEncoder;
this.jwtTokenProvider = jwtTokenProvider;
this.quizlyUserDetailsService = quizlyDetailsService;
this.notificationService = notificationService;
}

@PostMapping("/login")
Expand Down Expand Up @@ -168,6 +169,7 @@ public ResponseEntity<?> signup(@ModelAttribute SignupRequest signupRequest) {
LOGGER.error("Image is null: {}", e.getMessage());
}
LOGGER.info(imageBase64EncodedStr);
String signupToken = UUID.randomUUID().toString(); // make this secure
com.morris.quizly.models.security.UserDetails userDetails = com.morris.quizly.models.security.UserDetails.builder()
.firstName(signupRequest.getFirstName())
.lastName(signupRequest.getLastName())
Expand All @@ -179,12 +181,14 @@ public ResponseEntity<?> signup(@ModelAttribute SignupRequest signupRequest) {
.accountNonLocked(true)
.accountNonExpired(true)
.credentialsNonExpired(true)
.enabled(true)
.enabled(false)
.signupToken(signupToken)
.build();

try {
UserDetails userDetailsResult = quizlyUserDetailsService.save(userDetails);
if (userDetailsResult.isEnabled()) {
if (userDetailsResult.isAccountNonLocked()) {
notificationService.sendSignupConfirmationEmailAndLink(userDetailsResult, signupToken);
return ResponseEntity.ok()
.body(SIGNUP_SUCCESS);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.morris.quizly.controllers;

import com.morris.quizly.models.security.UserDetails;
import com.morris.quizly.services.QuizlyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/subscriptions")
public class SubscriptionController {

private final QuizlyUserDetailsService userService;

@Autowired
public SubscriptionController(QuizlyUserDetailsService userService) {
this.userService = userService;
}

@GetMapping("/confirm-signup")
public ResponseEntity<String> confirmSignup(@RequestParam("token") String token) {
UserDetails user = userService.findBySignupToken(token);
if (null == user) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid token");
}
userService.updateEnabledStatus(user, true);

String htmlResponse = "<html>" +
"<head>" +
"<style>" +
"body { font-family: Arial, sans-serif; text-align: center; padding: 20px; }" +
"h2 { color: #333; }" +
".logo { margin: 20px auto; }" +
".container { max-width: 600px; margin: 0 auto; text-align: center; }" +
".button { background-color: #000; color: #fff; padding: 10px 20px; text-decoration: none; display: inline-block; margin-top: 20px; }" +
"</style>" +
"</head>" +
"<body>" +
"<div class='container'>" +
"<img class='logo' src='/images/quizly-logo.png' alt='Quizly Logo'>" +
"<h2>Thanks for confirming signup with Quizly, " + user.getFirstName() + "!</h2>" +
"</div>" +
"<script>" +
"window.onload = function() {" +
" var newTab = window.open('', '_blank');" +
" newTab.document.write(\"<html><head><style>" +
"body { font-family: Arial, sans-serif; text-align: center; padding: 20px; }" +
"h1 { color: #333; }" +
".logo { margin: 20px auto; }" +
".container { max-width: 600px; margin: 0 auto; text-align: center; }" +
"</style></head><body><div class='container'>" +
"<h1>Subscription confirmed successfully!</h1>" +
"</div></body></html>\");" +
"};" +
"</script>" +
"</body>" +
"</html>";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_HTML);
return ResponseEntity.ok().headers(headers).body(htmlResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class UserDetails implements org.springframework.security.core.userdetail
private boolean credentialsNonExpired;
private boolean enabled;
private String lockedReason;
private String signupToken;

private int flagCount;
List<SystemFlag> flags;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ public interface UserRepository extends MongoRepository<UserDetails, String> {
*/
Optional<UserDetails> findByUsername(String username);

/**
* Find user by signup token. Signup tokens are used to confirm application subscriptions.
*
* @param token {@link String} signup token
*
* @return {@link UserDetails}
*/
Optional<UserDetails> findBySignupToken(String token);

/**
* Determines if an account is non-locked.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package com.morris.quizly.services;

public interface EmailService {

/**
* Sends email.
*
* @param to {@link String} email recipient
* @param subject {@link String} Subject
* @param body {@link String} email content
*/
void sendEmail(String to, String subject, String body);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.morris.quizly.services;

import com.morris.quizly.models.security.UserDetails;

public interface NotificationService {

/**
Expand All @@ -11,5 +13,11 @@ public interface NotificationService {
*/
boolean notifyAdmin(String message);

boolean subscribeUserToAppUserTopic(String userEmailAddress);
/**
* Sends signup confirmation email to user.
*
* @param user {@link UserDetails}
* @param token {@link String} confirmation token
*/
void sendSignupConfirmationEmailAndLink(UserDetails user, String token);
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,20 @@ public interface QuizlyUserDetailsService extends UserDetailsService {
* @return boolean
*/
boolean isUserNameInUse(String username);

/**
* Find user by signup token.
*
* @param token {@link String} signup token
* @return {@link UserDetails}
*/
UserDetails findBySignupToken(String token);

/**
* Modifies user enabled status.
*
* @param user {@link UserDetails} user
* @param enabled boolean
*/
void updateEnabledStatus(UserDetails user, boolean enabled);
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,63 @@
package com.morris.quizly.services.impl;

import com.morris.quizly.services.EmailService;
import com.sendgrid.Method;
import com.sendgrid.Request;
import com.sendgrid.Response;
import com.sendgrid.SendGrid;
import com.sendgrid.helpers.mail.Mail;
import com.sendgrid.helpers.mail.objects.ClickTrackingSetting;
import com.sendgrid.helpers.mail.objects.Content;
import com.sendgrid.helpers.mail.objects.Email;
import com.sendgrid.helpers.mail.objects.TrackingSettings;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Properties;
import java.io.IOException;

@Service
public class EmailServiceImpl implements EmailService {
private static final Logger LOGGER = LoggerFactory.getLogger(EmailServiceImpl.class);

@Value("$gmail.username")
private String admin;

@Value("${gmail.password}")
private String password;
@Value("${sendgrid.signup.confirmation.api}")
private String key;

@Override
public void sendEmail(String to, String subject, String body) {
Properties props = new Properties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.host", "smtp.gmail.com");
props.put("mail.smtp.port", "587");

Session session = Session.getInstance(props,
new javax.mail.Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(admin, password);
}
});
Mail mail = getMail(to, subject, body);

SendGrid sg = new SendGrid(key);
Request request = new Request();
try {
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(admin));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
message.setSubject(subject);
message.setText(body);

Transport.send(message);

System.out.println("Email sent successfully");
} catch (MessagingException e) {
throw new RuntimeException(e);
request.setMethod(Method.POST);
request.setEndpoint("mail/send");
request.setBody(mail.build());
Response response = sg.api(request);
System.out.println(response.getStatusCode());
System.out.println(response.getBody());
System.out.println(response.getHeaders());
} catch (IOException e) {
LOGGER.error("Error sending email: {}", e.getMessage());
}
}

@NotNull
private Mail getMail(String to, String subject, String body) {
Email from = new Email("ai.quizly@gmail.com");
Email toEmail = new Email(to);
Content content = new Content("text/plain", body);
Mail mail = new Mail(from, subject, toEmail, content);

// Disable click tracking
TrackingSettings trackingSettings = new TrackingSettings();
ClickTrackingSetting clickTrackingSetting = new ClickTrackingSetting();
clickTrackingSetting.setEnable(false);
clickTrackingSetting.setEnableText(false);
trackingSettings.setClickTrackingSetting(clickTrackingSetting);
mail.setTrackingSettings(trackingSettings);
return mail;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
import com.amazonaws.services.sns.AmazonSNSClientBuilder;
import com.amazonaws.services.sns.model.PublishRequest;
import com.amazonaws.services.sns.model.PublishResult;
import com.amazonaws.services.sns.model.SubscribeRequest;
import com.amazonaws.services.sns.model.SubscribeResult;
import com.morris.quizly.models.security.UserDetails;
import com.morris.quizly.services.EmailService;
import com.morris.quizly.services.NotificationService;
import org.slf4j.Logger;
Expand All @@ -20,12 +19,11 @@ public class NotificationServiceImpl implements NotificationService {
@Value("${sns.systemsFlaggingTopicArn}")
private String snsSystemsFlaggingTopicArn;

@Value("${sns.applicationUserTopicArn}")
private String snsApplicationUserTopicArn;

private final AmazonSNS amazonSNSClient;
private final EmailService emailService;

private static final String DEV_CONFIRMATION_LINK = "http://localhost:8081/api/subscriptions/confirm-signup?token=";

public NotificationServiceImpl(EmailService emailService) {
this.amazonSNSClient = AmazonSNSClientBuilder.defaultClient();
this.emailService = emailService;
Expand All @@ -39,20 +37,20 @@ public boolean notifyAdmin(String message) {
}

@Override
public boolean subscribeUserToAppUserTopic(String userEmailAddress) {
public void sendSignupConfirmationEmailAndLink(UserDetails user, String token) {
try {
SubscribeRequest subscribeRequest = new SubscribeRequest()
.withTopicArn(snsApplicationUserTopicArn)
.withProtocol("email")
.withEndpoint(userEmailAddress);
SubscribeResult subscribeResult = amazonSNSClient.subscribe(subscribeRequest);
sendCustomConfirmationEmail(userEmailAddress, subscribeResult.getSubscriptionArn());
return true;
sendCustomConfirmationEmail(user, token);
} catch (Exception e) {
LOGGER.error("Error subscribing user to Application User SNS Topic: {}", e.getMessage());
return false;
LOGGER.error("Error sending signup confirmation email: {}", e.getMessage());
}
}

private void sendCustomConfirmationEmail(String userEmail, String subscriptionArn) {}
private void sendCustomConfirmationEmail(UserDetails user, String token) {
String confirmationLink = DEV_CONFIRMATION_LINK + token;
String emailBody = String.format(
"Welcome to Quizly, %s! We're excited you've decided to join this educational journey with us.\n" +
"Please confirm your subscription by clicking the link below:\n\n" + confirmationLink, user.getFirstName()
);
emailService.sendEmail(user.getEmailAddress(), "Quizly Signup Confirmation", emailBody);
}
}
Loading

0 comments on commit f232253

Please sign in to comment.