diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 34d5ccd..609a421 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -11,6 +11,6 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 81c302f..62b6423 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 969f6be..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index a7709bc..b4bbb6b 100644 --- a/build.gradle +++ b/build.gradle @@ -55,6 +55,7 @@ dependencies { compile group: 'org.springframework.boot', name: 'spring-boot-starter-tomcat', version: springBootVersion compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-sleuth', version: springSleuthVersion compile group: 'org.springframework.data', name: 'spring-data-mongodb', version: springMongoDataVersion + compile group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: springBootVersion compile group: 'org.mongodb', name: 'mongodb-driver-sync', version: javaMongoDriverVersion compile group: 'com.google.code.gson', name: 'gson', version: gsonVersion compile group: 'com.google.guava', name: 'guava', version: guavaVersion diff --git a/src/main/java/TechVault/services/User/EmailService.java b/src/main/java/TechVault/services/User/EmailService.java new file mode 100644 index 0000000..bd490b0 --- /dev/null +++ b/src/main/java/TechVault/services/User/EmailService.java @@ -0,0 +1,18 @@ +package TechVault.services.User; + +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 +public class EmailService { + @Autowired + private JavaMailSender mailSender; + + @Async + public void sendEmail(SimpleMailMessage email) { + mailSender.send(email); + } +} \ No newline at end of file diff --git a/src/main/java/TechVault/services/User/UserService.java b/src/main/java/TechVault/services/User/UserService.java new file mode 100644 index 0000000..58982ec --- /dev/null +++ b/src/main/java/TechVault/services/User/UserService.java @@ -0,0 +1,35 @@ +package TechVault.services.User; + +import TechVault.services.User.model.User; +import TechVault.services.User.request.ChangePasswordRequest; +import TechVault.services.User.request.PasswordForgotRequest; +import TechVault.services.User.request.UserLoginRequest; +import TechVault.services.User.response.UserResponse; + +import java.nio.file.AccessDeniedException; + +public interface UserService { + boolean register(User user); + + User fetchUser(String email) throws AccessDeniedException; + + User saveUser(User user); + + User resetUser(PasswordForgotRequest passwordForgotRequest); + + void changeUserPassword(ChangePasswordRequest changePasswordRequest); + + void confirmUser(String token); + + String loginUser(UserLoginRequest userLoginRequest); + + void logoutUser(String sessionId); + + UserResponse getUser(String sessionId); + + boolean userExistsByEmail(String email); + + boolean userExistsByUserName(String userName); + + Boolean getVerificationStatus(User user) throws AccessDeniedException; +} diff --git a/src/main/java/TechVault/services/User/UserServiceImpl.java b/src/main/java/TechVault/services/User/UserServiceImpl.java new file mode 100644 index 0000000..d3736c7 --- /dev/null +++ b/src/main/java/TechVault/services/User/UserServiceImpl.java @@ -0,0 +1,206 @@ +package TechVault.services.User; + +import TechVault.services.User.exception.InvalidArgumentException; +import TechVault.services.User.exception.ResourceNotFoundException; +import TechVault.services.User.model.Session; +import TechVault.services.User.model.User; +import TechVault.services.User.persistence.SessionRepository; +import TechVault.services.User.persistence.UserRepository; +import TechVault.services.User.request.ChangePasswordRequest; +import TechVault.services.User.request.PasswordForgotRequest; +import TechVault.services.User.request.UserLoginRequest; +import TechVault.services.User.response.UserResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.nio.file.AccessDeniedException; +import java.security.SecureRandom; +import java.util.Objects; +import java.util.Optional; +import java.util.Random; +import java.util.UUID; + +@Slf4j +@Service +public class UserServiceImpl implements UserService { + @Autowired + private UserRepository userRepository; + + @Autowired + private SessionRepository sessionRepository; + + private static final Random RANDOM = new SecureRandom(); + private static final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + @Override + public boolean register(User user) throws InvalidArgumentException { + String password = user.getPassword(); + if (password.isEmpty()) { + throw new InvalidArgumentException("Invalid password."); + } + if (user.getUserName().isEmpty()) { + user.setUserName(user.getEmail()); + } + + User userExists = userRepository.findByUserName(user.getUserName()); + + if (userExists != null) { + throw new InvalidArgumentException(user.getUserName() + " already registered."); + } + + if (userRepository.findByEmail(user.getEmail()) != null) { + throw new InvalidArgumentException(user.getEmail() + " already registered."); + } + + // Disable user until they click on confirmation link in email + user.setEmailVerified(0); + user.setId(RANDOM.nextLong()); + // Generate random 36-character string token for confirmation link + user.setConfirmationToken(UUID.randomUUID().toString()); + userRepository.save(user); + + return true; + } + + @Override + public User resetUser(PasswordForgotRequest passwordForgotRequest) { + + User userExists = userRepository.findByEmail(passwordForgotRequest.getEmail()); + + if (userExists == null) { + throw new InvalidArgumentException(passwordForgotRequest.getEmail() + " is not registered."); + } + + String password = generatePassword(10); + userExists.setPassword(password); + userExists.setTempPassword(true); + + userRepository.save(userExists); + + // return the user with plain password so that we can send it to the user's email. + userExists.setPassword(password); + + return userExists; + } + + @Override + public void changeUserPassword(ChangePasswordRequest changePasswordRequest) { + User userExists = userRepository.findByUserName(changePasswordRequest.getUserName()); + + if (userExists == null) { + throw new InvalidArgumentException(changePasswordRequest.getUserName() + " is not registered."); + } + + if (userExists.getEmailVerified() == 0) { + throw new InvalidArgumentException("The user is not enabled."); + } + + userExists.setPassword(changePasswordRequest.getPassword()); + + userRepository.save(userExists); + } + + @Override + public void confirmUser(String token) { + User user = userRepository.findByConfirmationToken(token); + + if (user == null) { + throw new InvalidArgumentException("Invalid token."); + } + // Token found + user.setEmailVerified(1); + user.setConfirmationToken(""); + + // Save user + userRepository.save(user); + } + + @Override + public String loginUser(UserLoginRequest userLoginRequest) { + if (userLoginRequest.getEmail() == null && userLoginRequest.getUserName() == null) { + throw new InvalidArgumentException("Required either email or user name."); + } + User userExists = null; + if (userLoginRequest.getUserName() != null) { + userExists = userRepository.findByUserName(userLoginRequest.getUserName()); + } else { + userExists = userRepository.findByEmail(userLoginRequest.getEmail()); + } + if (userExists == null) { + throw new InvalidArgumentException("Invalid user name."); + } + String password = userLoginRequest.getPassword(); + if (!password.equals(userExists.getPassword())) { + throw new InvalidArgumentException("Invalid user name and password combination."); + } + if (userExists.getEmailVerified() == 0) { + throw new InvalidArgumentException("The user is not enabled."); + } + + Long user_id = userExists.getId(); + String sessionId = UUID.randomUUID().toString(); + Session session = new Session(); + session.setId(sessionId); + session.setUser_id(user_id); + sessionRepository.save(session); + return sessionId; + } + + @Override + public void logoutUser(String sessionId) { + Session session = sessionRepository.findById(sessionId).get(); + sessionRepository.delete(session); + } + + @Override + public UserResponse getUser(String sessionId) { + Session session = sessionRepository.findById(sessionId).get(); + User user = userRepository.findById(session.getUser_id()).get(); + return new UserResponse(user.getUserName(), user.getEmailVerified()); + } + + @Override + public User fetchUser(String email) throws AccessDeniedException { + if (Objects.isNull(email)) { + throw new AccessDeniedException("Invalid access"); + } + User user = userRepository.findByEmail(email); + if (user == null) { + throw new ResourceNotFoundException("User not found"); + } + return user; + } + + @Override + public User saveUser(User user) { + if (Objects.isNull(user)) { + throw new InvalidArgumentException("Null user"); + } + + return userRepository.save(user); + } + + @Override + public boolean userExistsByEmail(String email) { + return userRepository.existsByEmail(email); + } + + @Override + public boolean userExistsByUserName(String userName) { + return userRepository.existsByUserName(userName); + } + + @Override + public Boolean getVerificationStatus(User user) throws AccessDeniedException { + return user.getEmailVerified() == 1; + } + + public static String generatePassword(int length) { + StringBuilder returnValue = new StringBuilder(length); + for (int i = 0; i < length; i++) { + returnValue.append(ALPHABET.charAt(RANDOM.nextInt(ALPHABET.length()))); + } + return new String(returnValue); + } +} diff --git a/src/main/java/TechVault/services/User/controller/UserController.java b/src/main/java/TechVault/services/User/controller/UserController.java new file mode 100644 index 0000000..aab4777 --- /dev/null +++ b/src/main/java/TechVault/services/User/controller/UserController.java @@ -0,0 +1,128 @@ +package TechVault.services.User.controller; + +import TechVault.services.User.EmailService; +import TechVault.services.User.UserService; +import TechVault.services.User.model.User; +import TechVault.services.User.request.ChangePasswordRequest; +import TechVault.services.User.request.PasswordForgotRequest; +import TechVault.services.User.request.RegisterUserRequest; +import TechVault.services.User.request.UserLoginRequest; +import TechVault.services.User.response.UserResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +@Slf4j +@Validated +@RestController +@RequestMapping("user") +public class UserController { + + @Autowired + private UserService userService; + @Autowired + private EmailService emailService; + @Value("${webServerUrl}") + private String webServerUrl; + + + @PostMapping(value = "/register") + public ResponseEntity registerUser(@RequestBody @Valid RegisterUserRequest registerUserRequest) { + User user = new User(); + user.setEmail(registerUserRequest.getEmail()); + user.setPassword(registerUserRequest.getPassword()); + user.setUserName(registerUserRequest.getUserName()); + try { + if (userService.register(user)) { + SimpleMailMessage registrationEmail = new SimpleMailMessage(); + registrationEmail.setTo(user.getEmail()); + registrationEmail.setSubject("Registration Confirmation"); + registrationEmail.setText("To confirm your e-mail address, please click the link below:\n" + webServerUrl + + "/user/confirm?token=" + user.getConfirmationToken()); + registrationEmail.setFrom("noreply@domain.com"); + + emailService.sendEmail(registrationEmail); + } + return new ResponseEntity<>("Registered.", HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @GetMapping(path = "/confirm") + public ResponseEntity confirm(@RequestParam("token") String token) { + try { + userService.confirmUser(token); + return new ResponseEntity<>("Confirmed.", HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @PostMapping(path = "/login") + public ResponseEntity login(@Valid @RequestBody UserLoginRequest userLoginRequest) { + try { + return new ResponseEntity<>(userService.loginUser(userLoginRequest), HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @PostMapping(path = "/logout") + public ResponseEntity logout(@RequestParam("sessionId") String sessionId) { + try { + userService.logoutUser(sessionId); + return new ResponseEntity<>("Successful", HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @PostMapping(path = "/session") + public ResponseEntity getUser(@RequestParam("sessionId") String sessionId) { + try { + UserResponse userResponse = userService.getUser(sessionId); + return new ResponseEntity<>(userResponse, HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @PostMapping(path = "/reset") + public ResponseEntity forgotPasswordRequest(@Valid @RequestBody PasswordForgotRequest passwordForgotRequest) { + try { + User resetUser = userService.resetUser(passwordForgotRequest); + if (resetUser != null) { + SimpleMailMessage registrationEmail = new SimpleMailMessage(); + registrationEmail.setTo(passwordForgotRequest.getEmail()); + registrationEmail.setSubject("Temporary Password Sent From TechVault"); + registrationEmail + .setText("To access your account, please use this temporary password: " + resetUser.getPassword() + + ".\r\nNOTE: This email was sent from an automated system. Please do not reply."); + registrationEmail.setFrom("noreply@domain.com"); + emailService.sendEmail(registrationEmail); + } + return new ResponseEntity<>("Check your email.", HttpStatus.OK); + } catch (Exception e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + } + + @PostMapping(path = "/changepwd") + public ResponseEntity changePassword(@Valid @RequestBody ChangePasswordRequest changePasswordRequest) { + userService.changeUserPassword(changePasswordRequest); + return new ResponseEntity<>("Changed.", HttpStatus.OK); + } +} diff --git a/src/main/java/TechVault/services/User/exception/InvalidArgumentException.java b/src/main/java/TechVault/services/User/exception/InvalidArgumentException.java new file mode 100644 index 0000000..9185e0c --- /dev/null +++ b/src/main/java/TechVault/services/User/exception/InvalidArgumentException.java @@ -0,0 +1,23 @@ +package TechVault.services.User.exception; + +public class InvalidArgumentException extends RuntimeException { + + private static final long serialVersionUID = -1262173968380116559L; + + public InvalidArgumentException() { + super(); + } + + public InvalidArgumentException(String s) { + super(s); + } + + public InvalidArgumentException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidArgumentException(Throwable cause) { + super(cause); + } + +} \ No newline at end of file diff --git a/src/main/java/TechVault/services/User/exception/ResourceNotFoundException.java b/src/main/java/TechVault/services/User/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..ec369fa --- /dev/null +++ b/src/main/java/TechVault/services/User/exception/ResourceNotFoundException.java @@ -0,0 +1,22 @@ +package TechVault.services.User.exception; + +public class ResourceNotFoundException extends RuntimeException { + private static final long serialVersionUID = 1177491237661223459L; + + public ResourceNotFoundException() { + super(); + } + + public ResourceNotFoundException(String s) { + super(s); + } + + public ResourceNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public ResourceNotFoundException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/TechVault/services/User/model/Session.java b/src/main/java/TechVault/services/User/model/Session.java new file mode 100644 index 0000000..8f047fb --- /dev/null +++ b/src/main/java/TechVault/services/User/model/Session.java @@ -0,0 +1,22 @@ +package TechVault.services.User.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +@Document(collection = "session_token") +@Getter +@Setter +@NoArgsConstructor +public class Session { + + @Id + @Field(name = "_id") + private String id; + + @Field(name = "user_id") + private Long user_id; +} diff --git a/src/main/java/TechVault/services/User/model/User.java b/src/main/java/TechVault/services/User/model/User.java new file mode 100644 index 0000000..0c09758 --- /dev/null +++ b/src/main/java/TechVault/services/User/model/User.java @@ -0,0 +1,35 @@ +package TechVault.services.User.model; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +@Document(collection = "user") +@Getter +@Setter +@NoArgsConstructor +public class User { + + @Id + @Field(name = "id") + private Long id; + + @Field(name = "email") + private String email; + + @Field(name = "password") + private String password; + + @Field(name = "user_name") + private String userName; + + @Field(name = "email_verified") + private Integer emailVerified; + + private String confirmationToken; + + private boolean isTempPassword; +} diff --git a/src/main/java/TechVault/services/User/persistence/SessionRepository.java b/src/main/java/TechVault/services/User/persistence/SessionRepository.java new file mode 100644 index 0000000..ab74b30 --- /dev/null +++ b/src/main/java/TechVault/services/User/persistence/SessionRepository.java @@ -0,0 +1,12 @@ +package TechVault.services.User.persistence; + +import TechVault.services.User.model.Session; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface SessionRepository extends MongoRepository { + Optional findById(String id); +} \ No newline at end of file diff --git a/src/main/java/TechVault/services/User/persistence/UserRepository.java b/src/main/java/TechVault/services/User/persistence/UserRepository.java new file mode 100644 index 0000000..e53a6e4 --- /dev/null +++ b/src/main/java/TechVault/services/User/persistence/UserRepository.java @@ -0,0 +1,22 @@ +package TechVault.services.User.persistence; + +import TechVault.services.User.model.User; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends MongoRepository { + User findByEmail(String email); + + Boolean existsByEmail(String email); + + Boolean existsByUserName(String userName); + + User findByUserName(String username); + + Optional findById(Long id); + + User findByConfirmationToken(String confirmationToken); +} \ No newline at end of file diff --git a/src/main/java/TechVault/services/User/request/ChangePasswordRequest.java b/src/main/java/TechVault/services/User/request/ChangePasswordRequest.java new file mode 100644 index 0000000..3253eb7 --- /dev/null +++ b/src/main/java/TechVault/services/User/request/ChangePasswordRequest.java @@ -0,0 +1,22 @@ +package TechVault.services.User.request; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Data +public class ChangePasswordRequest { + + @NotBlank + @Size(min = 3, max = 52) + private String userName; + + @NotBlank + @Size(min = 6, max = 52) + private String password; + + @NotBlank + @Size(min = 6, max = 52) + private String passwordRepeat; +} diff --git a/src/main/java/TechVault/services/User/request/PasswordForgotRequest.java b/src/main/java/TechVault/services/User/request/PasswordForgotRequest.java new file mode 100644 index 0000000..e1c1f6b --- /dev/null +++ b/src/main/java/TechVault/services/User/request/PasswordForgotRequest.java @@ -0,0 +1,15 @@ +package TechVault.services.User.request; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Data +public class PasswordForgotRequest { + + @NotBlank + @Size(min = 3, max = 52) + private String email; +} + diff --git a/src/main/java/TechVault/services/User/request/RegisterUserRequest.java b/src/main/java/TechVault/services/User/request/RegisterUserRequest.java new file mode 100644 index 0000000..b96e570 --- /dev/null +++ b/src/main/java/TechVault/services/User/request/RegisterUserRequest.java @@ -0,0 +1,26 @@ +package TechVault.services.User.request; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Data +public class RegisterUserRequest { + + @NotBlank + @Size(min = 3, max = 52) + private String email; + + @NotBlank + @Size(min = 3, max = 52) + private String userName; + + @NotBlank + @Size(min = 6, max = 52) + private String password; + + @NotBlank + @Size(min = 6, max = 52) + private String passwordRepeat; +} diff --git a/src/main/java/TechVault/services/User/request/UserLoginRequest.java b/src/main/java/TechVault/services/User/request/UserLoginRequest.java new file mode 100644 index 0000000..1df5833 --- /dev/null +++ b/src/main/java/TechVault/services/User/request/UserLoginRequest.java @@ -0,0 +1,21 @@ +package TechVault.services.User.request; + + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Data +public class UserLoginRequest { + + @Size(min = 3, max = 52) + private String email; + + @Size(min = 3, max = 52) + private String userName; + + @NotBlank + @Size(min = 6, max = 52) + private String password; +} diff --git a/src/main/java/TechVault/services/User/response/UserResponse.java b/src/main/java/TechVault/services/User/response/UserResponse.java new file mode 100644 index 0000000..bb383f4 --- /dev/null +++ b/src/main/java/TechVault/services/User/response/UserResponse.java @@ -0,0 +1,14 @@ +package TechVault.services.User.response; + +import lombok.Data; + +@Data +public class UserResponse { + private String userName; + private Integer emailVerified; + + public UserResponse(String userName, Integer emailVerified) { + this.userName = userName; + this.emailVerified = emailVerified; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 29d097e..3d017ae 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,16 @@ #spring.application.name: TechVault server.port= 8080 spring.data.mongodb.uri=mongodb+srv://prateek:p@cluster0.kf3n4.mongodb.net/TechVault?retryWrites=true&w=majority -spring.data.mongodb.database=TechVault \ No newline at end of file +spring.data.mongodb.database=TechVault + + +spring.mail.host=smtp.gmail.com +spring.mail.username=techvaulttech@gmail.com +spring.mail.password=wediscusstech +spring.mail.port=587 +spring.mail.properties.mail.smtp.starttls.enable=true +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.required=true + +#Used in the emails sent to users +webServerUrl=http://localhost:8080 \ No newline at end of file