Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
Merge pull request #8 from BudgetBuddyDE/4
Browse files Browse the repository at this point in the history
feat(BB-4): Implement reset-password logic
  • Loading branch information
tklein1801 authored Dec 26, 2023
2 parents 841f0b0 + f5ed83c commit 105cf56
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 25 deletions.
79 changes: 79 additions & 0 deletions src/main/java/de/budgetbuddy/backend/MailService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package de.budgetbuddy.backend;

import de.budgetbuddy.backend.user.User;
import jakarta.annotation.Nullable;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
public class MailService {
private final Environment environment;

@Autowired
public MailService(Environment environment) {
this.environment = environment;
}

@Nullable
public String getMailServiceHost() {
return environment.getProperty("de.budget-buddy.mail-service.address");
}

/**
* Triggers the mail service
* @return boolean
*/
public Boolean trigger(JSONObject payload) {
String mailServiceUrl = getMailServiceHost();
if (mailServiceUrl == null) {
throw new RuntimeException("Mail-service host-url is not set");
}

return WebhookTrigger.send(mailServiceUrl + "/send", payload.toString());
}

/**
* Returns the payload for the verification mail
* @return JSONObject
*/
public static JSONObject getVerificationMailPayload(User user) {
JSONObject payload = new JSONObject();
payload.put("mail", "welcome");
payload.put("to", user.getEmail());
payload.put("uuid", user.getUuid().toString());

return payload;
}

/**
* Returns the payload for the password reset mail
* @return JSONObject
*/
public static JSONObject getRequestPasswordMailPayload(String email, UUID uuid, UUID otp) {
JSONObject payload = new JSONObject();
payload.put("mail", "reset_password");
payload.put("to", email);
payload.put("uuid", uuid.toString());
payload.put("otp", otp.toString());

return payload;
}

/**
* Returns the payload for the password changed mail
* @return JSONObject
*/
public static JSONObject getPasswordChangedMailPayload(String email, String name, String targetEmailAddress) {
JSONObject payload = new JSONObject();
payload.put("mail", "password_changed");
payload.put("to", email);
payload.put("name", name);
payload.put("targetMailAddress", targetEmailAddress);

return payload;
}
}
149 changes: 131 additions & 18 deletions src/main/java/de/budgetbuddy/backend/auth/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import de.budgetbuddy.backend.ApiResponse;
import de.budgetbuddy.backend.WebhookTrigger;
import de.budgetbuddy.backend.MailService;
import de.budgetbuddy.backend.log.Log;
import de.budgetbuddy.backend.log.LogType;
import de.budgetbuddy.backend.log.Logger;
Expand All @@ -13,16 +13,17 @@
import de.budgetbuddy.backend.user.role.Role;
import de.budgetbuddy.backend.user.role.RolePermission;
import jakarta.servlet.http.HttpSession;
import org.json.JSONObject;
import org.mindrot.jbcrypt.BCrypt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.view.RedirectView;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
Expand All @@ -32,12 +33,17 @@
public class AuthController {
private final ObjectMapper objMapper = new ObjectMapper().registerModule(new JavaTimeModule());
private final UserRepository userRepository;
private final Environment environment;
private final UserPasswordResetRepository userPasswordResetRepository;
private final MailService mailService;

@Autowired
public AuthController(UserRepository userRepository, Environment env) {
public AuthController(
UserRepository userRepository,
UserPasswordResetRepository userPasswordResetRepository,
MailService mailService) {
this.userRepository = userRepository;
this.environment = env;
this.userPasswordResetRepository = userPasswordResetRepository;
this.mailService = mailService;
}

@PostMapping(value = "/register")
Expand Down Expand Up @@ -94,19 +100,13 @@ public ResponseEntity<ApiResponse<User>> register(
}

User savedUser = userRepository.save(user);

if (!Objects.isNull(environment)) {
String mailServiceHost = environment.getProperty("de.budget-buddy.mail-service.address");
if (!Objects.isNull(mailServiceHost) && mailServiceHost.length() > 1) {
JSONObject payload = new JSONObject();
payload.put("to", savedUser.getEmail());
payload.put("mail", "welcome");
payload.put("uuid", savedUser.getUuid().toString());
Boolean wasVerificationMailSent = WebhookTrigger.send(mailServiceHost + "/send", payload.toString());
if (!wasVerificationMailSent) {
Logger.getInstance().log(new Log("Backend", LogType.WARNING, "Registration", "Couldn't send email-verification-mail"));
}
try {
if (!mailService.trigger(MailService.getVerificationMailPayload(savedUser))) {
Logger.getInstance()
.log(new Log("Backend", LogType.WARNING, "registration", "Couldn't send the verification email"));
}
} catch (Exception e) {
System.out.print(e.getMessage());
}

return ResponseEntity
Expand Down Expand Up @@ -209,4 +209,117 @@ public ResponseEntity<ApiResponse<String>> logoutUser(HttpSession session) {
.status(HttpStatus.OK)
.body(new ApiResponse<>(HttpStatus.OK.value(), "Your session has been destroyed"));
}

@PostMapping("/password/request-reset")
public ResponseEntity<ApiResponse<UserPasswordReset>> requestPasswordReset(
@RequestParam String email) {
Optional<User> optUser = userRepository.findByEmail(email);

if (optUser.isEmpty()) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ApiResponse<>(HttpStatus.NOT_FOUND.value(), "No user found for the provided email"));
}

UserPasswordReset passwordReset = userPasswordResetRepository.save(new UserPasswordReset(optUser.get()));

try {
if (!mailService.trigger(MailService.getRequestPasswordMailPayload(
email,
optUser.get().getUuid(),
passwordReset.getOtp()))) {
Logger.getInstance()
.log(new Log("Backend", LogType.WARNING, "password-reset", "Couldn't send the request-password-change email"));
}
} catch (Exception e) {
System.out.print(e.getMessage());
}

return ResponseEntity
.status(HttpStatus.OK)
.body(new ApiResponse<>(passwordReset));
}

@PostMapping("/password/validate-otp")
public ResponseEntity<ApiResponse<Boolean>> validatePasswordRequestOtp(
@RequestParam UUID otp
) {
ApiResponse<Boolean> validationResult = this.isOtpValid(otp);
return ResponseEntity
.status(validationResult.getStatus())
.body(validationResult);
}

@PostMapping("/password/reset")
public ResponseEntity<ApiResponse<User>> resetPassword(
@RequestParam UUID otp,
@RequestParam String newPassword
) {
ApiResponse<Boolean> validationResult = this.isOtpValid(otp);
if (!validationResult.getData()) {
return ResponseEntity
.status(validationResult.getStatus())
.body(new ApiResponse<>(validationResult.getStatus(), validationResult.getMessage()));
}

UserPasswordReset passwordReset = userPasswordResetRepository.findByOtp(otp).get();
passwordReset.setUsed(true);
userPasswordResetRepository.save(passwordReset);

User user = passwordReset.getOwner();
user.setPassword(newPassword);
user.hashPassword();
userRepository.save(user);

try {
if (!mailService.trigger(MailService.getPasswordChangedMailPayload(
user.getEmail(),
user.getName(),
user.getEmail()))) {
Logger.getInstance()
.log(new Log("Backend", LogType.WARNING, "password-reset", "Couldn't send the password-changed notification email"));
}
} catch (Exception e) {
System.out.print(e.getMessage());
}

return ResponseEntity
.status(HttpStatus.OK)
.body(new ApiResponse<>(user));
}

public ApiResponse<Boolean> isOtpValid(UUID otp) {
Optional<UserPasswordReset> optPasswordRequest = userPasswordResetRepository.findByOtp(otp);
if (optPasswordRequest.isEmpty()) {
return new ApiResponse<>(
HttpStatus.NOT_FOUND.value(),
"No session found for the provided session",
false);
}

UserPasswordReset passwordReset = optPasswordRequest.get();
LocalDateTime now = LocalDateTime.now();
LocalDateTime otpCreatedAt = passwordReset
.getCreatedAt()
.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();

if (ChronoUnit.DAYS.between(otpCreatedAt, now) >= 1) {
return new ApiResponse<>(
HttpStatus.UNAUTHORIZED.value(),
"This OTP has expired",
false);
}

if (passwordReset.wasUsed()) {
return new ApiResponse<>(
HttpStatus.UNAUTHORIZED.value(),
"This OTP has already been used",
false);
}

return new ApiResponse<>(true);
}

}
47 changes: 47 additions & 0 deletions src/main/java/de/budgetbuddy/backend/auth/UserPasswordReset.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package de.budgetbuddy.backend.auth;

import de.budgetbuddy.backend.user.User;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.ColumnDefault;

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

@Entity
@Table(name = "user_password_reset", schema = "public")
@Data
public class UserPasswordReset {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;

@ManyToOne
@JoinColumn(name = "owner")
private User owner;

@Column(name = "otp")
private UUID otp;

@Column(name = "used")
private Boolean used;

@Column(name = "created_at", nullable = false, updatable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
@ColumnDefault("CURRENT_TIMESTAMP")
private Date createdAt = new Date();

public UserPasswordReset() {}

public UserPasswordReset(User user) {
this.owner = user;
this.otp = UUID.randomUUID();
this.used = false;
}

public boolean wasUsed() {
return used;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package de.budgetbuddy.backend.auth;

import de.budgetbuddy.backend.user.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

public interface UserPasswordResetRepository extends JpaRepository<UserPasswordReset, Long> {
Optional<UserPasswordReset> findByOtp(UUID otp);
List<UserPasswordReset> findAllByOwner(User user);
}
2 changes: 1 addition & 1 deletion src/main/java/de/budgetbuddy/backend/user/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class User {
@Column(name = "is_verified", nullable = false)
private Boolean isVerified = false;

@OneToOne
@ManyToOne
@JoinColumn(name = "role")
private Role role;

Expand Down
18 changes: 12 additions & 6 deletions src/test/java/de/budgetbuddy/backend/auth/AuthControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import de.budgetbuddy.backend.ApiResponse;
import de.budgetbuddy.backend.MailService;
import de.budgetbuddy.backend.user.User;
import de.budgetbuddy.backend.user.UserRepository;
import de.budgetbuddy.backend.user.role.Role;
import de.budgetbuddy.backend.user.role.RolePermission;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpSession;
Expand All @@ -25,13 +25,19 @@
import static org.mockito.Mockito.*;

class AuthControllerTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private AuthController authController;
private final UserRepository userRepository;
private final AuthController authController;
private MockHttpSession session;
private final ObjectMapper objMapper = new ObjectMapper().registerModule(new JavaTimeModule());

AuthControllerTest() {
this.userRepository = mock(UserRepository.class);
UserPasswordResetRepository userPasswordResetRepository = mock(UserPasswordResetRepository.class);
Environment environment = mock(Environment.class);
MailService mailService = new MailService(environment);
this.authController = new AuthController(userRepository, userPasswordResetRepository, mailService);
}

@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
Expand Down

0 comments on commit 105cf56

Please sign in to comment.