Full Stack Reddit Clone - Spring Boot, React, Electron App - Part 3
MaxiCB
Posted on July 28, 2020
Full Stack Reddit Clone - Spring Boot, React, Electron App - Part 3
Introduction
Welcome to Part 3 of creating a Reddit clone using Spring Boot, and React.
What are we building in this part?
- Spring Security
- Registration Logic
- Registration Endpoint
- Password Encoding
- Activation Emails
- Verification/Activation Endpoint
In Part 2 we created all of entities and repositories needed within our backend!
Important Links
- Backend Source: https://github.com/MaxiCB/vox-nobis/tree/master/backend
- Frontend Source: https://github.com/MaxiCB/vox-nobis/tree/master/client
- Live URL: In Progress
Part 1: Spring Security π
Let's cover the different configuration classes we will need. Inside com.your-name.backend create a new package called config, and add the following classes.
- Security: Handles the security configuration for the whole application, and handles encoding the password before storing it into the database.
package com.maxicb.backend.config;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebSecurity
public class Security extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**")
.permitAll()
.anyRequest()
.authenticated();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
- Constants: Defines the link to the activation endpoint that will be sent inside the account activation email.
package com.maxicb.backend.config;
import lombok.experimental.UtilityClass;
@UtilityClass
public class Constants {
public static final String EMAIL_ACTIVATION = "http://localhost:8080/api/auth/verify";
}
Part 2: Registration Request Data Transfer Object - DTO π
Let's cover the different DTO classes we will need. Inside com.your-name.backend create a new package called dto, and add the following classes.
- RegisterRequest: Defines the data that our backend will recieve from the client during a registration request.
package com.maxicb.backend.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RegisterRequest {
private String username;
private String email;
private String password;
}
Part 3: Activation Exception Creation π
Let's cover all of the custom exceptions our application will have. Inside com.your-name.backend create a new package called exception, and add the following classes.
- ActivationException: Custom exception to handle errors when sending users activation emails
package com.maxicb.backend.exception;
public class ActivationException extends RuntimeException {
public ActivationException(String message) {
super(message);
}
}
Part 4: Email Builder π
Let's cover all of the different email building classes our application will have. Inside com.your-name.backend create a new package called service, and add the following classes.
We also need to add the @EnableAsync annotation to our BackendApplication.java class to reduce the amount of time the user is waiting during registration. The reason this is needed is due to the registration endpoint hanging when sending the account activation email.
- BackendApplication - Updated:
package com.maxicb.backend;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class BackendApplication {
public static void main(String[] args) {
SpringApplication.run(BackendApplication.class, args);
}
}
- MailBuilder: Holds the logic to create our email using a HTML template we will create later.
package com.maxicb.backend.service;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
@Service
@AllArgsConstructor
public class MailBuilder {
TemplateEngine templateEngine;
String build(String message) {
Context context = new Context();
context.setVariable("body", message);
return templateEngine.process("mailTemplate", context);
}
}
- MailService: Holds the logic to send a user an account activation email.
package com.maxicb.backend.service;
import com.maxicb.backend.exception.ActivationException;
import com.maxicb.backend.model.NotificationEmail;
import lombok.AllArgsConstructor;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
@AllArgsConstructor
public class MailService {
JavaMailSender javaMailSender;
MailBuilder mailBuilder;
@Async
void sendEmail(NotificationEmail notificationEmail) {
MimeMessagePreparator messagePreparator = mimeMessage -> {
MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage);
messageHelper.setFrom("activation@redditclone.com");
messageHelper.setTo(notificationEmail.getRecepient());
messageHelper.setSubject(notificationEmail.getSubject());
messageHelper.setText(mailBuilder.build(notificationEmail.getBody()));
};
try {
javaMailSender.send(messagePreparator);
System.out.println("Activation Email Sent");
} catch (MailException e) {
throw new ActivationException("Error sending activation email to " + notificationEmail.getRecepient());
}
}
}
Part 5: Email Template π§
Let's add the HTML email template our application will use for account activation. Inside resources.templates create a new file called mailTemplate.html, and add the following template.
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head></head>
<body>
<span th:text="${body}"></span>
</body>
</html>
Part 6: Authentication Service π
Let's cover all of the different authentication services our application will have. Inside com.your-name.backend.services add the following class.
- AuthService: Holds the logic to register a user and store them inside the database, encoding of users passwords, verifying tokens, and enabling of accounts.
package com.maxicb.backend.service;
import com.maxicb.backend.dto.RegisterRequest;
import com.maxicb.backend.exception.ActivationException;
import com.maxicb.backend.model.AccountVerificationToken;
import com.maxicb.backend.model.NotificationEmail;
import com.maxicb.backend.model.User;
import com.maxicb.backend.repository.TokenRepository;
import com.maxicb.backend.repository.UserRepository;
import lombok.AllArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import static com.maxicb.backend.config.Constants.EMAIL_ACTIVATION;
@Service
@AllArgsConstructor
public class AuthService {
UserRepository userRepository;
PasswordEncoder passwordEncoder;
TokenRepository tokenRepository;
MailService mailService;
MailBuilder mailBuilder;
@Transactional
public void register(RegisterRequest registerRequest) {
User user = new User();
user.setUsername(registerRequest.getUsername());
user.setEmail(registerRequest.getEmail());
user.setPassword(encodePassword(registerRequest.getPassword()));
user.setCreationDate(Instant.now());
user.setAccountStatus(false);
userRepository.save(user);
String token = generateToken(user);
String message = mailBuilder.build("Welcome to React-Spring-Reddit Clone. " +
"Please visit the link below to activate you account : " + EMAIL_ACTIVATION + "/" + token);
mailService.sendEmail(new NotificationEmail("Please Activate Your Account", user.getEmail(), message));
}
private String encodePassword(String password) {
return passwordEncoder.encode(password);
}
private String generateToken(User user) {
String token = UUID.randomUUID().toString();
AccountVerificationToken verificationToken = new AccountVerificationToken();
verificationToken.setToken(token);
verificationToken.setUser(user);
tokenRepository.save(verificationToken);
return token;
}
public void verifyToken(String token) {
Optional<AccountVerificationToken> verificationToken = tokenRepository.findByToken(token);
verificationToken.orElseThrow(() -> new ActivationException("Invalid Activation Token"));
enableAccount(verificationToken.get());
}
public void enableAccount(AccountVerificationToken token) {
String username = token.getUser().getUsername();
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new ActivationException("User not found with username: " + username));
user.setAccountStatus(true);
userRepository.save(user);
}
}
Part 7: Authentication Controller π
Let's add the authentication controller our application will use. Inside com.your-name.backend create a new package called controller, and add the following classes..
- AuthController: Defines the different endpoints for registering a user, and activating account's when user's visit the activation link sent in the email.
package com.maxicb.backend.controller;
import com.maxicb.backend.dto.RegisterRequest;
import com.maxicb.backend.service.AuthService;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
@AllArgsConstructor
public class AuthController {
AuthService authService;
@PostMapping("/register")
public ResponseEntity register(@RequestBody RegisterRequest registerRequest) {
authService.register(registerRequest);
return new ResponseEntity<>(HttpStatus.OK);
}
@GetMapping("/verify/{token}")
public ResponseEntity verify(@PathVariable String token) {
authService.verifyToken(token);
return new ResponseEntity<>("Account Activated", HttpStatus.OK);
}
}
Conclusion π
- To ensure everything is configured correctly you can run the application, and ensure there are no error in the console. Towards the bottom of the console you should see output similar to below
- If there are no error's in the console you can test you registration logic by sending a post request to http://localhost:8080/api/auth/register with the following data
{
"username": "test",
"email": "test1@test.com",
"password": "test12345"
}
Once you recieve a 200 OK status back you can check you mailtrap.io inbox to find the activation email that was sent. The link should look similar to http://localhost:8080/api/auth/verify/{token}, be sure to omit the < from the end of the link. Navigation to the link will activate the account, and you should see "Account Activated" displayed as a response.
In this article we added Spring Security, user password encoding, sending account activation emails, and created the logic and endpoints to handle account registration, and activation.
After creating all of the different classes, and writing all of the code your project structure should look similar to below
Next Part 4
Posted on July 28, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.