Skip to content

Commit

Permalink
Implement email verification
Browse files Browse the repository at this point in the history
only allow verified users to use the anime entry routes

Signed-off-by: Sayan Biswas <me@sayanbiswas.in>
  • Loading branch information
Dank-del committed Apr 29, 2023
1 parent c5ffadf commit ac32358
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 10 deletions.
5 changes: 4 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/barta/myanimelounge/AppConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.barta.myanimelounge;

import com.barta.myanimelounge.interceptors.IsVerifiedInterceptor;
import com.barta.myanimelounge.repository.UserRepository;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer {
private final UserRepository userRepository;

public AppConfig(UserRepository userRepository) {
this.userRepository = userRepository;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new IsVerifiedInterceptor(userRepository))
.excludePathPatterns("/auth/**", "/api/test/**");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import java.util.Objects;

@RestController
@RequestMapping(value = "/api/entries/anime")
@RequestMapping(value = "/entries/anime")
public class AnimeEntryController {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
Expand Down Expand Up @@ -71,7 +71,7 @@ public AnimeEntry createAnimeEntryForCurrentUser(@Valid @RequestBody() AnimeEntr

@PutMapping("/{id}")
@PreAuthorize("hasRole('USER') or hasRole('MODERATOR') or hasRole('ADMIN')")
public AnimeEntry updateAnimeEntryForCurrentUser(@Valid @RequestBody() AnimeEntry animeEntry, @PathVariable("id") String Id) throws ChangeSetPersister.NotFoundException {
public AnimeEntry updateAnimeEntryForCurrentUser(@Valid @RequestBody() AnimeEntry animeEntry, @PathVariable("id") String Id) {
boolean validStatus = false;
for (String status : statuses) {
if (status.equals(animeEntry.getStatus())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@

import javax.validation.Valid;

import com.barta.myanimelounge.models.ConfirmationToken;
import com.barta.myanimelounge.models.ERole;
import com.barta.myanimelounge.repository.ConfirmationTokenRepository;
import com.barta.myanimelounge.services.EmailService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import com.barta.myanimelounge.models.Role;
import com.barta.myanimelounge.models.User;
Expand All @@ -35,14 +35,17 @@

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
@RequestMapping("/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;

@Autowired
UserRepository userRepository;

@Autowired
ConfirmationTokenRepository confirmationTokenRepository;

@Autowired
RoleRepository roleRepository;

Expand All @@ -52,6 +55,9 @@ public class AuthController {
@Autowired
JwtUtils jwtUtils;

@Autowired
EmailService emailService;

@PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {

Expand Down Expand Up @@ -129,6 +135,29 @@ public ResponseEntity<?> registerUser(@Valid @RequestBody SignupRequest signUpRe
user.setRoles(roles);
userRepository.save(user);

ConfirmationToken confirmationToken = new ConfirmationToken(user);
confirmationTokenRepository.save(confirmationToken);
SimpleMailMessage mailMessage = new SimpleMailMessage();
mailMessage.setTo(user.getEmail());
mailMessage.setSubject("Complete Registration!");
mailMessage.setText("To confirm your account, please click here : "
+ "http://localhost:8080/auth/confirm-account?token=" + confirmationToken.getConfirmationToken());
emailService.sendEmail(mailMessage);

System.out.println("Confirmation Token: " + confirmationToken.getConfirmationToken());
return ResponseEntity.ok(new MessageResponse("User registered successfully!"));
}

@RequestMapping(value = "/confirm-account", method = {RequestMethod.GET, RequestMethod.POST})
public ResponseEntity<?> confirmUserAccount(@RequestParam("token") String confirmationToken) {
ConfirmationToken token = confirmationTokenRepository.findByConfirmationToken(confirmationToken);

if (token != null) {
User user = userRepository.findUserByEmailIgnoreCase(token.getUserEntity().getEmail());
user.setEmailVerified(true);
userRepository.save(user);
return ResponseEntity.ok("Email verified successfully!");
}
return ResponseEntity.badRequest().body("Error: Couldn't verify email");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.barta.myanimelounge.interceptors;

import com.barta.myanimelounge.exceptions.EntryNotFoundException;
import com.barta.myanimelounge.models.User;
import com.barta.myanimelounge.repository.UserRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class IsVerifiedInterceptor implements HandlerInterceptor {
private final UserRepository userRepository;

public IsVerifiedInterceptor(UserRepository userRepository) {
this.userRepository = userRepository;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null) {
User user = userRepository.findByUsername(authentication.getName())
.orElseThrow(EntryNotFoundException::new);
System.out.println(user);
if (!user.isEmailVerified()) {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter()
.write("{\"message\": \"Email verification is required to call this controller\"}");
response.setStatus(HttpStatus.FORBIDDEN.value());
return false;
}
}
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.barta.myanimelounge.models;

import java.util.Date;
import java.util.UUID;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
@Table(name="confirmationToken")
public class ConfirmationToken {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="token_id")
private Long tokenId;

@Column(name="confirmation_token")
private String confirmationToken;

@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;

@OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
@JoinColumn(nullable = false, name = "user_id")
private User user;

public ConfirmationToken() {}

public ConfirmationToken(User user) {
this.user = user;
createdDate = new Date();
confirmationToken = UUID.randomUUID().toString();
}

public Long getTokenId() {
return tokenId;
}

public void setTokenId(Long tokenId) {
this.tokenId = tokenId;
}

public String getConfirmationToken() {
return confirmationToken;
}

public void setConfirmationToken(String confirmationToken) {
this.confirmationToken = confirmationToken;
}

public Date getCreatedDate() {
return createdDate;
}

public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}

public User getUserEntity() {
return user;
}

public void setUserEntity(User user) {
this.user = user;
}


}
11 changes: 11 additions & 0 deletions src/main/java/com/barta/myanimelounge/models/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ public class User {
@Size(max = 120)
private String password;

public boolean isEmailVerified() {
return emailVerified;
}

public void setEmailVerified(boolean emailVerified) {
this.emailVerified = emailVerified;
}

@Column(columnDefinition = "boolean default false")
private boolean emailVerified;

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<AnimeEntry> animeEntries;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.barta.myanimelounge.repository;

import com.barta.myanimelounge.models.ConfirmationToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository("confirmationTokenRepository")
public interface ConfirmationTokenRepository extends JpaRepository<ConfirmationToken, Long> {
ConfirmationToken findByConfirmationToken(String confirmationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
Boolean existsByUsername(String username);

Boolean existsByEmail(String email);

User findUserByEmailIgnoreCase(String emailId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers("/api/auth/**").permitAll()
.authorizeRequests().antMatchers("/auth/**").permitAll()
.antMatchers("/api/test/**").permitAll()
.anyRequest().authenticated();

Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/barta/myanimelounge/services/EmailService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.barta.myanimelounge.services;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service("emailService")
public class EmailService {

private final JavaMailSender javaMailSender;

@Autowired
public EmailService(JavaMailSender javaMailSender) {
this.javaMailSender = javaMailSender;
}

@Async
public void sendEmail(SimpleMailMessage email) {
javaMailSender.send(email);
}

}

0 comments on commit ac32358

Please sign in to comment.