Skip to content

Commit

Permalink
added mail server and implemented whats needed for password recovery
Browse files Browse the repository at this point in the history
  • Loading branch information
Wassim-Rached committed Oct 24, 2024
1 parent 4437ad1 commit d8d3284
Show file tree
Hide file tree
Showing 15 changed files with 345 additions and 50 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

<!-- devtools spring-->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

import com.example.usermanagement.dto.accounts.*;
import com.example.usermanagement.entities.Account;
import com.example.usermanagement.events.publishers.emails.EmailVerificationTokenGeneratedEvent;
import com.example.usermanagement.events.publishers.emails.PasswordResetGeneratedEvent;
import com.example.usermanagement.interfaces.services.IAccountService;
import com.example.usermanagement.interfaces.services.IEmailService;
import com.example.usermanagement.interfaces.services.IEmailVerificationTokenService;
import jakarta.persistence.EntityExistsException;
import com.example.usermanagement.interfaces.services.IPasswordResetTokenService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -21,8 +25,11 @@ public class AccountController {

private final IAccountService accountService;
private final IEmailVerificationTokenService emailVerificationTokenService;
private final IPasswordResetTokenService passwordResetTokenService;
private final IEmailService emailService;
private final ApplicationEventPublisher eventPublisher;

// auth related
// account management related
@PostMapping
public ResponseEntity<UUID> createAccount(@RequestBody CreateAccountDTO requestBody) {

Expand All @@ -32,49 +39,12 @@ public ResponseEntity<UUID> createAccount(@RequestBody CreateAccountDTO requestB
// generate email verification token
String token = emailVerificationTokenService.generateEmailVerificationToken(userAccount);

// TODO: send email with token
String body = "Click here to verify your email: http://localhost:8080/api/accounts/verify-email?token=" + token;
emailService.sendEmail("wa55death405@gmail.com", "Email verification", body);

return new ResponseEntity<>(userAccount.getId(), HttpStatus.CREATED);
}

@GetMapping("/verify-email")
public ResponseEntity<String> verifyEmail(@RequestParam String token) {
String email = emailVerificationTokenService.consumeEmailVerificationToken(token);
accountService.verifyAccountEmail(email);
return new ResponseEntity<>("Email verified", HttpStatus.OK);
}

// resend email verification token
@PostMapping("/verify-email/resend")
public ResponseEntity<String> resendEmailVerificationToken(@RequestParam String email) {
Account account = accountService.getAccountByEmail(email);
String token = emailVerificationTokenService.generateEmailVerificationToken(account);

// TODO: send email with token

return new ResponseEntity<>("Email verification token sent", HttpStatus.OK);
}

@PostMapping("/reset-password/request")
public ResponseEntity<String> resetPassword(@RequestParam String email) {
Account account = accountService.getAccountByEmail(email);
accountService.requestResetPassword(account);
return new ResponseEntity<>("Password reset", HttpStatus.OK);
}

@PostMapping("/reset-password/confirm")
public ResponseEntity<String> resetPassword(@RequestBody ResetPasswordRequest requestBody) {
accountService.resetPassword(requestBody.getToken(), requestBody.getNewPassword());
return new ResponseEntity<>("Password reset", HttpStatus.OK);
}

@PostMapping("/change-password")
public ResponseEntity<String> changePassword(@RequestBody ChangePasswordRequest requestBody) {
accountService.changeMyPassword(requestBody.getOldPassword(), requestBody.getNewPassword());
return new ResponseEntity<>("Password changed", HttpStatus.OK);
}

// info related
@GetMapping
public ResponseEntity<Page<GeneralAccountDTO>> getAccounts(
@RequestParam(required = false) String email,
Expand Down Expand Up @@ -111,6 +81,51 @@ public ResponseEntity<List<AccountAuthoritiesEditResponse>> editAuthorities(@Req
return new ResponseEntity<>(res, HttpStatus.OK);
}


// email verification related
@GetMapping("/verify-email")
public ResponseEntity<String> verifyEmail(@RequestParam String token) {
String email = emailVerificationTokenService.consumeEmailVerificationToken(token);
accountService.verifyAccountEmail(email);
return new ResponseEntity<>("Email verified", HttpStatus.OK);
}

@PostMapping("/verify-email/resend")
public ResponseEntity<String> resendEmailVerificationToken(@RequestParam String email) {
Account account = accountService.getAccountByEmail(email);
String token = emailVerificationTokenService.generateEmailVerificationToken(account);

eventPublisher.publishEvent(new EmailVerificationTokenGeneratedEvent(this, token, email));

return new ResponseEntity<>("Email verification token sent", HttpStatus.OK);
}


// password related
@PostMapping("/reset-password/resend")
public ResponseEntity<String> requestResetPassword(@RequestParam String email) {
Account account = accountService.getAccountByEmail(email);
String token = passwordResetTokenService.generatePasswordResetToken(account);

eventPublisher.publishEvent(new PasswordResetGeneratedEvent(this, email, token));

return new ResponseEntity<>("Password reset token sent", HttpStatus.OK);
}

@PostMapping("/reset-password")
public ResponseEntity<String> confirmResetPassword(@RequestBody ResetPasswordRequest requestBody) {
accountService.resetPassword(requestBody.getToken(), requestBody.getNewPassword());
return new ResponseEntity<>("Password reset", HttpStatus.OK);
}

@PostMapping("/change-password")
public ResponseEntity<String> changePassword(@RequestBody ChangePasswordRequest requestBody) {
accountService.changeMyPassword(requestBody.getOldPassword(), requestBody.getNewPassword());
return new ResponseEntity<>("Password changed", HttpStatus.OK);
}


// special info management related
@PostMapping("/{accountId}/identity-verification")
public ResponseEntity<String> verifyIdentity(@RequestParam boolean verify, @PathVariable UUID accountId) {
Account account = accountService.getAccountById(accountId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.example.usermanagement.entities;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.Instant;
import java.util.UUID;

@Entity
@Getter
@Setter
@Table(name = "password_reset_tokens")
@NoArgsConstructor
public class PasswordResetToken {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

@Column(nullable = false, unique = true)
private String token;

@Column(nullable = false, name = "expiry_date")
private Instant expiryDate;

@Column(nullable = false, name = "created_date")
private Instant createdDate;

@Column(nullable = false, name = "is_used")
private Boolean isUsed;

@OneToOne(fetch = FetchType.LAZY)
private Account account;

public PasswordResetToken(Account account, int expiryTimeInSec) {
this.token = UUID.randomUUID().toString();
this.createdDate = Instant.now();
this.expiryDate = this.createdDate.plusSeconds(expiryTimeInSec);
this.isUsed = false;
this.account = account;
}

public boolean isExpired() {
return Instant.now().isAfter(this.expiryDate);
}

public boolean haveBeenCreatedLately(int seconds) {
return Instant.now().isBefore(this.createdDate.plusSeconds(seconds));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.example.usermanagement.events.listeners;


import com.example.usermanagement.events.publishers.emails.EmailVerificationTokenGeneratedEvent;
import com.example.usermanagement.events.publishers.emails.PasswordHaveBeenResetedEvent;
import com.example.usermanagement.events.publishers.emails.PasswordResetGeneratedEvent;
import com.example.usermanagement.interfaces.services.IEmailService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

// TODO: make it use thymeleaf for email templates

@Component
@RequiredArgsConstructor
public class EmailsListener {

@Value("${app.clients.web.amwc}")
private String amwc;

private final IEmailService emailService;

@EventListener(EmailVerificationTokenGeneratedEvent.class)
public void onEmailVerificationTokenGeneratedEvent(EmailVerificationTokenGeneratedEvent event) {
String link = amwc + "/api/accounts/verify-email?token=" + event.getToken();
String body = "Click here to verify your email: " + link;
emailService.sendEmail("wa55death405@gmail.com", "Email verification", body);
}

@EventListener(PasswordResetGeneratedEvent.class)
public void onPasswordResetGeneratedEvent(PasswordResetGeneratedEvent event) {
String link = amwc + "/api/accounts/reset-password?token=" + event.getToken();
String body = "Click here to reset your password: " + link;
emailService.sendEmail(event.getEmail(), "Password reset", body);
}

@EventListener(PasswordHaveBeenResetedEvent.class)
public void onPasswordHaveBeenResetedEvent(PasswordHaveBeenResetedEvent event) {
String body = "Your password have been reseted";
emailService.sendEmail(event.getEmail(), "Password reseted", body);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.usermanagement.events.publishers.emails;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

@Getter
public class EmailVerificationTokenGeneratedEvent extends ApplicationEvent{
private final String token;
private final String email;

public EmailVerificationTokenGeneratedEvent(Object source, String token, String email) {
super(source);
this.token = token;
this.email = email;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.usermanagement.events.publishers.emails;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

@Getter
public class PasswordHaveBeenResetedEvent extends ApplicationEvent {
private final String email;

public PasswordHaveBeenResetedEvent(Object source, String email) {
super(source);
this.email = email;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.example.usermanagement.events.publishers.emails;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

@Getter
public class PasswordResetGeneratedEvent extends ApplicationEvent {
private final String email;
private final String token;

public PasswordResetGeneratedEvent(Object source, String email, String token) {
super(source);
this.email = email;
this.token = token;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public interface IAccountService {
Account getMyAccount();
Account updateMyAccount(UpdateAccountDTO requestBody);

void requestResetPassword(Account account);
void resetPassword(String token, String newPassword);
void changeMyPassword(String oldPassword, String newPassword);
void verifyAccountEmail(String accountEmail);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.usermanagement.interfaces.services;

public interface IEmailService {
void sendEmail(String to, String subject, String body);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.usermanagement.interfaces.services;

import com.example.usermanagement.entities.Account;

public interface IPasswordResetTokenService {
String generatePasswordResetToken(Account account);
String consumePasswordResetToken(String token);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.usermanagement.repositories;

import com.example.usermanagement.entities.Account;
import com.example.usermanagement.entities.PasswordResetToken;
import org.springframework.data.jpa.repository.JpaRepository;

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

public interface PasswordResetTokenRepository extends JpaRepository<PasswordResetToken, UUID> {
Optional<PasswordResetToken> findByToken(String token);
Optional<PasswordResetToken> findByAccount(Account account);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.example.usermanagement.entities.Role;
import com.example.usermanagement.entities.Account;
import com.example.usermanagement.events.publishers.*;
import com.example.usermanagement.events.publishers.emails.PasswordHaveBeenResetedEvent;
import com.example.usermanagement.exceptions.ForbiddenException;
import com.example.usermanagement.interfaces.services.IAccountService;
import com.example.usermanagement.repositories.PermissionRepository;
Expand Down Expand Up @@ -67,16 +68,15 @@ public void verifyAccountEmail(String accountEmail) {
eventPublisher.publishEvent(event);
}

@Override
public void requestResetPassword(Account account) {
// TODO: generate,persist and send reset password token
throw new UnsupportedOperationException("Not implemented");
}

@Override
public void resetPassword(String token, String newPassword) {
// TODO: validate token and change password
throw new UnsupportedOperationException("Not implemented");
Account account = getMyAccount();

account.setPassword(passwordEncoder.encode(newPassword));
accountRepository.save(account);

var event = new PasswordHaveBeenResetedEvent(this,account.getEmail());
eventPublisher.publishEvent(event);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.usermanagement.services;

import com.example.usermanagement.interfaces.services.IEmailService;
import lombok.RequiredArgsConstructor;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;


@Service
@RequiredArgsConstructor
public class EmailService implements IEmailService {
private final JavaMailSender mailSender;

public void sendEmail(String to, String subject, String body) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject(subject);
message.setText(body);
message.setFrom("isetch-google-club@gmail.com");
mailSender.send(message);
}
}
Loading

0 comments on commit d8d3284

Please sign in to comment.