Skip to content

Commit f149ceb

Browse files
authored
Merge pull request #193 from nramc/192-feature-adopt-email-verification-resource-for-latest-plan
feat: email address removed from AuthUser and tests adopted
2 parents e52458b + 2f169bc commit f149ceb

21 files changed

+102
-72
lines changed

src/main/java/com/github/nramc/dev/journey/api/config/ApplicationServiceConfig.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,20 @@ public UserSecurityEmailAddressAttributeService userSecurityAttributesProvider(
1919
}
2020

2121
@Bean
22-
public EmailCodeValidator emailCodeValidator(ConfirmationCodeRepository codeRepository) {
23-
return new EmailCodeValidator(codeRepository);
22+
public EmailCodeValidator emailCodeValidator(
23+
ConfirmationCodeRepository codeRepository,
24+
UserSecurityEmailAddressAttributeService emailAddressAttributeService) {
25+
return new EmailCodeValidator(codeRepository, emailAddressAttributeService);
2426
}
2527

2628
@Bean
2729
public EmailConfirmationCodeService emailCodeService(
2830
MailService mailService,
2931
ConfirmationCodeRepository codeRepository,
30-
EmailCodeValidator codeValidator) {
32+
EmailCodeValidator codeValidator,
33+
UserSecurityEmailAddressAttributeService emailAddressAttributeService) {
3134

32-
return new EmailConfirmationCodeService(mailService, codeRepository, codeValidator);
35+
return new EmailConfirmationCodeService(mailService, codeRepository, codeValidator, emailAddressAttributeService);
3336
}
3437

3538
}

src/main/java/com/github/nramc/dev/journey/api/repository/auth/AuthUser.java

-2
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,11 @@ public class AuthUser implements UserDetails {
2626
private String username;
2727
private String password;
2828
private String name;
29-
private String emailAddress;
3029
private String secret;
3130
private LocalDateTime createdDate;
3231
private LocalDateTime lastLoggedIn;
3332
private boolean enabled;
3433
private Set<Role> roles;
35-
private boolean isEmailAddressVerified;
3634

3735
@Override
3836
public Collection<? extends GrantedAuthority> getAuthorities() {

src/main/java/com/github/nramc/dev/journey/api/services/email/EmailCodeValidator.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,21 @@
55
import com.github.nramc.dev.journey.api.repository.security.ConfirmationCodeEntity;
66
import com.github.nramc.dev.journey.api.repository.security.ConfirmationCodeRepository;
77
import com.github.nramc.dev.journey.api.services.confirmationcode.ConfirmationCode;
8+
import com.github.nramc.dev.journey.api.web.dto.user.security.UserSecurityAttribute;
9+
import com.github.nramc.dev.journey.api.web.resources.rest.users.security.email.UserSecurityEmailAddressAttributeService;
810
import lombok.RequiredArgsConstructor;
911
import lombok.extern.slf4j.Slf4j;
1012

1113
import java.time.Duration;
1214
import java.time.LocalDateTime;
15+
import java.util.Optional;
1316

1417
@RequiredArgsConstructor
1518
@Slf4j
1619
public class EmailCodeValidator {
1720
static final int EMAIL_CODE_VALIDITY_MINUTES = 15;
1821
private final ConfirmationCodeRepository codeRepository;
22+
private final UserSecurityEmailAddressAttributeService emailAddressAttributeService;
1923

2024
/**
2125
* @param confirmationCode code to be validated
@@ -25,7 +29,12 @@ public class EmailCodeValidator {
2529
public boolean isValid(ConfirmationCode confirmationCode, AuthUser authUser) {
2630
EmailCode emailCode = (EmailCode) confirmationCode;
2731
ConfirmationCodeEntity confirmationCodeEntity = codeRepository.findByUsernameAndCode(authUser.getUsername(), emailCode.code());
32+
Optional<UserSecurityAttribute> emailAttributeIfExists = emailAddressAttributeService.provideEmailAttributeIfExists(authUser);
2833

34+
if (emailAttributeIfExists.isEmpty()) {
35+
log.info("Email Code verification failed. Reason:[email address not exists]");
36+
return false;
37+
}
2938
if (confirmationCodeEntity == null) { // Email Code does not exists for user
3039
log.info("Email Code verification failed. Reason:[code not exists]");
3140
return false;
@@ -34,7 +43,7 @@ public boolean isValid(ConfirmationCode confirmationCode, AuthUser authUser) {
3443
log.info("Email Code verification failed. Reason:[code not active]");
3544
return false;
3645
}
37-
if (!confirmationCodeEntity.getReceiver().equals(authUser.getEmailAddress())) { // Email Address not matched
46+
if (!confirmationCodeEntity.getReceiver().equals(emailAttributeIfExists.get().value())) { // Email Address not matched
3847
log.info("Email Code verification failed. Reason:[Email address not matched]");
3948
return false;
4049
}

src/main/java/com/github/nramc/dev/journey/api/services/email/EmailConfirmationCodeService.java

+19-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
import com.github.nramc.dev.journey.api.services.confirmationcode.ConfirmationCode;
99
import com.github.nramc.dev.journey.api.services.confirmationcode.ConfirmationCodeService;
1010
import com.github.nramc.dev.journey.api.services.confirmationcode.ConfirmationUseCase;
11+
import com.github.nramc.dev.journey.api.web.dto.user.security.UserSecurityAttribute;
12+
import com.github.nramc.dev.journey.api.web.exceptions.BusinessException;
1113
import com.github.nramc.dev.journey.api.web.exceptions.TechnicalException;
14+
import com.github.nramc.dev.journey.api.web.resources.rest.users.security.email.UserSecurityEmailAddressAttributeService;
1215
import jakarta.mail.MessagingException;
1316
import lombok.RequiredArgsConstructor;
1417
import lombok.extern.slf4j.Slf4j;
@@ -32,6 +35,7 @@ public class EmailConfirmationCodeService implements ConfirmationCodeService {
3235
private final MailService mailService;
3336
private final ConfirmationCodeRepository codeRepository;
3437
private final EmailCodeValidator emailCodeValidator;
38+
private final UserSecurityEmailAddressAttributeService emailAddressAttributeService;
3539

3640
/**
3741
* Generate Email code securely
@@ -44,15 +48,22 @@ public class EmailConfirmationCodeService implements ConfirmationCodeService {
4448
@Override
4549
public void send(AuthUser authUser, ConfirmationUseCase useCase) {
4650

51+
UserSecurityAttribute emailAttribute = getUserEmailSecurityAttribute(authUser);
52+
4753
EmailCode emailCode = generateEmailCode();
4854

49-
sendEmailCode(useCase, emailCode, authUser);
55+
sendEmailCode(useCase, emailCode, authUser, emailAttribute);
5056

51-
saveEmailCode(useCase, emailCode, authUser);
57+
saveEmailCode(useCase, emailCode, authUser, emailAttribute);
5258

5359
log.info("Email Code has been sent to registered email address");
5460
}
5561

62+
private UserSecurityAttribute getUserEmailSecurityAttribute(AuthUser authUser) {
63+
return emailAddressAttributeService.provideEmailAttributeIfExists(authUser)
64+
.orElseThrow(() -> new BusinessException("email.not.exists", "Email not registered"));
65+
}
66+
5667
/**
5768
* Verify given Email Code for authenticated user.
5869
* In order to consider code is valid, below conditions should be satisfied
@@ -68,6 +79,7 @@ public boolean verify(ConfirmationCode confirmationCode, AuthUser authUser, Conf
6879
boolean isEmailCodeValid = emailCodeValidator.isValid(confirmationCode, authUser);
6980

7081
if (isEmailCodeValid) {
82+
emailAddressAttributeService.setVerifiedStatus(true, authUser);
7183
invalidateAllCodes(authUser);
7284
log.info("Email Code verified successfully and all codes invalidated");
7385
}
@@ -80,25 +92,26 @@ EmailCode generateEmailCode() {
8092
return EmailCode.valueOf(code);
8193
}
8294

83-
private void sendEmailCode(ConfirmationUseCase useCase, EmailCode emailCode, AuthUser authUser) {
95+
private void sendEmailCode(ConfirmationUseCase useCase, EmailCode emailCode, AuthUser authUser,
96+
UserSecurityAttribute emailAttribute) {
8497
try {
8598
Map<String, Object> parameters = new HashedMap<>();
8699
parameters.put("name", authUser.getName());
87100
parameters.put("ottPin", emailCode.code());
88101

89-
mailService.sendEmailUsingTemplate(EMAIL_CODE_TEMPLATE_HTML, authUser.getEmailAddress(), getSubject(useCase), parameters);
102+
mailService.sendEmailUsingTemplate(EMAIL_CODE_TEMPLATE_HTML, emailAttribute.value(), getSubject(useCase), parameters);
90103
} catch (RuntimeException | MessagingException ex) {
91104
throw new TechnicalException("Unable to send Email Code", ex);
92105
}
93106
}
94107

95-
private void saveEmailCode(ConfirmationUseCase useCase, EmailCode code, AuthUser authUser) {
108+
private void saveEmailCode(ConfirmationUseCase useCase, EmailCode code, AuthUser authUser, UserSecurityAttribute emailAttribute) {
96109
ConfirmationCodeEntity entity = ConfirmationCodeEntity.builder()
97110
.id(UUID.randomUUID().toString())
98111
.type(ConfirmationCodeType.EMAIL_CODE)
99112
.code(code.code())
100113
.username(authUser.getUsername())
101-
.receiver(authUser.getEmailAddress())
114+
.receiver(emailAttribute.value())
102115
.isActive(true)
103116
.createdAt(LocalDateTime.now().minusMinutes(2))
104117
.useCase(useCase)

src/main/java/com/github/nramc/dev/journey/api/web/dto/user/UserConverter.java

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ public static User toUser(AuthUser authUser) {
2020
.name(authUser.getName())
2121
.username(authUser.getUsername())
2222
.roles(authUser.getRoles())
23-
.emailAddress(authUser.getEmailAddress())
2423
.lastLoggedIn(authUser.getLastLoggedIn())
2524
.createdDate(authUser.getCreatedDate())
2625
.enabled(authUser.isEnabled())

src/main/java/com/github/nramc/dev/journey/api/web/resources/rest/users/create/CreateUserRequest.java

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.github.nramc.dev.journey.api.web.resources.rest.users.create;
22

33
import com.github.nramc.dev.journey.api.security.Role;
4-
import jakarta.validation.constraints.Email;
54
import jakarta.validation.constraints.NotBlank;
65
import jakarta.validation.constraints.NotEmpty;
76
import jakarta.validation.constraints.Size;
@@ -12,7 +11,6 @@ public record CreateUserRequest(
1211
@NotBlank @Size(min = 8, max = 50) String username,
1312
@NotBlank @Size(min = 8, max = 50) String password,
1413
@NotBlank @Size(min = 3, max = 50) String name,
15-
@Email String emailAddress,
1614
@NotEmpty Set<Role> roles
1715
) {
1816
}

src/main/java/com/github/nramc/dev/journey/api/web/resources/rest/users/create/CreateUserResource.java

-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ private AuthUser toUserModel(CreateUserRequest userRequest) {
4848
.username(userRequest.username())
4949
.password(passwordEncoder.encode(userRequest.password()))
5050
.roles(userRequest.roles())
51-
.emailAddress(userRequest.emailAddress())
5251
.build();
5352
}
5453

src/main/java/com/github/nramc/dev/journey/api/web/resources/rest/users/security/email/UserSecurityEmailAddressAttributeService.java

+18
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.github.nramc.dev.journey.api.repository.auth.UserSecurityAttributesRepository;
88
import com.github.nramc.dev.journey.api.web.dto.user.security.UserSecurityAttribute;
99
import com.github.nramc.dev.journey.api.web.dto.user.security.UserSecurityAttributeConverter;
10+
import com.github.nramc.dev.journey.api.web.exceptions.BusinessException;
1011
import com.github.nramc.dev.journey.api.web.resources.rest.users.security.utils.SecurityAttributesUtils;
1112
import lombok.RequiredArgsConstructor;
1213
import lombok.extern.slf4j.Slf4j;
@@ -54,4 +55,21 @@ public UserSecurityAttribute saveSecurityEmailAddress(AuthUser authUser, EmailAd
5455
return UserSecurityAttributeConverter.toModel(savedEntity);
5556
}
5657

58+
public void setVerifiedStatus(boolean status, AuthUser authUser) {
59+
List<UserSecurityAttributeEntity> attributesEntities = userSecurityAttributesRepository
60+
.findAllByUserIdAndType(authUser.getId().toHexString(), SecurityAttributeType.EMAIL_ADDRESS);
61+
62+
UserSecurityAttributeEntity emailAttribute = Optional.of(attributesEntities)
63+
.filter(CollectionUtils::isNotEmpty)
64+
.map(List::getFirst)
65+
.orElseThrow(() -> new BusinessException("Email Security Attribute not exists", "email.not.exists"));
66+
67+
UserSecurityAttributeEntity updatedAttribute = emailAttribute.toBuilder()
68+
.enabled(status)
69+
.verified(status)
70+
.lastUpdateDate(LocalDate.now())
71+
.build();
72+
userSecurityAttributesRepository.save(updatedAttribute);
73+
}
74+
5775
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.github.nramc.dev.journey.api.web.resources.rest.users.security.email;
1+
package com.github.nramc.dev.journey.api.web.resources.rest.users.security.email.code;
22

33
import com.fasterxml.jackson.annotation.JsonProperty;
44
import jakarta.validation.constraints.Digits;
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.github.nramc.dev.journey.api.web.resources.rest.users.security.email;
1+
package com.github.nramc.dev.journey.api.web.resources.rest.users.security.email.code;
22

33
import com.github.nramc.dev.journey.api.repository.auth.AuthUser;
44
import com.github.nramc.dev.journey.api.services.email.EmailCode;
@@ -19,7 +19,6 @@
1919
import org.springframework.web.bind.annotation.RequestBody;
2020
import org.springframework.web.bind.annotation.RestController;
2121

22-
import java.util.Objects;
2322
import java.util.Optional;
2423

2524
import static com.github.nramc.dev.journey.api.services.confirmationcode.ConfirmationUseCase.VERIFY_EMAIL_ADDRESS;
@@ -45,15 +44,11 @@ public ResponseEntity<Void> verifyEmailCode(Authentication authentication,
4544
.map(AuthUser.class::cast)
4645
.orElseThrow(() -> new AccessDeniedException("User does not exists"));
4746

48-
Objects.requireNonNull(authUser.getEmailAddress(), "Email address not exists for the user");
49-
5047
boolean isValid = emailConfirmationCodeService.verify(
5148
EmailCode.valueOf(Integer.parseInt(request.code())),
5249
authUser, VERIFY_EMAIL_ADDRESS);
5350

5451
if (isValid) {
55-
AuthUser updatedUserDetails = authUser.toBuilder().isEmailAddressVerified(true).build();
56-
userDetailsService.updateUser(updatedUserDetails);
5752
log.info("Email Code has been verified successfully");
5853
return ResponseEntity.ok().build();
5954
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.github.nramc.dev.journey.api.web.resources.rest.users.security.email;
1+
package com.github.nramc.dev.journey.api.web.resources.rest.users.security.email.code;
22

33
import com.github.nramc.dev.journey.api.repository.auth.AuthUser;
44
import com.github.nramc.dev.journey.api.services.email.EmailConfirmationCodeService;
@@ -15,7 +15,6 @@
1515
import org.springframework.web.bind.annotation.PostMapping;
1616
import org.springframework.web.bind.annotation.RestController;
1717

18-
import java.util.Objects;
1918
import java.util.Optional;
2019

2120
import static com.github.nramc.dev.journey.api.services.confirmationcode.ConfirmationUseCase.VERIFY_EMAIL_ADDRESS;
@@ -40,7 +39,6 @@ public void sendEmailCode(Authentication authentication) {
4039
.map(AuthUser.class::cast)
4140
.orElseThrow(() -> new AccessDeniedException("User does not exists"));
4241

43-
Objects.requireNonNull(authUser.getEmailAddress(), "Email address not exists for the user");
4442
emailConfirmationCodeService.send(authUser, VERIFY_EMAIL_ADDRESS);
4543

4644
log.info("Email Code has been sent successfully");
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
package com.github.nramc.dev.journey.api.web.resources.rest.users.update;
22

3-
import jakarta.validation.constraints.Email;
43
import jakarta.validation.constraints.NotBlank;
54
import jakarta.validation.constraints.Size;
65

76
public record UpdateUserRequest(
8-
@NotBlank @Size(min = 3, max = 50) String name,
9-
@NotBlank @Email String emailAddress) {
7+
@NotBlank @Size(min = 3, max = 50) String name) {
108
}

src/main/java/com/github/nramc/dev/journey/api/web/resources/rest/users/update/UpdateUserResource.java

-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ public void change(@RequestBody @Valid UpdateUserRequest updateUserRequest, Auth
3737
private AuthUser updateWith(AuthUser authUser, UpdateUserRequest request) {
3838
return authUser.toBuilder()
3939
.name(request.name())
40-
.emailAddress(request.emailAddress())
4140
.build();
4241
}
4342
}

src/test/java/com/github/nramc/dev/journey/api/config/security/WebSecurityTestConfig.java

-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ public UserDetailsManager testUserDetailsService(PasswordEncoder passwordEncoder
3939
.password(passwordEncoder.encode("test"))
4040
.roles(Set.of(Role.AUTHENTICATED_USER))
4141
.name("Authenticated User")
42-
.emailAddress("[email protected]")
4342
.build();
4443
return new InMemoryUserDetailsManager(testUser, authenticatedUser, admin) {
4544
@Override

src/test/java/com/github/nramc/dev/journey/api/services/email/EmailCodeValidatorTest.java

+10-9
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@
44
import com.github.nramc.dev.journey.api.repository.auth.AuthUser;
55
import com.github.nramc.dev.journey.api.repository.security.ConfirmationCodeEntity;
66
import com.github.nramc.dev.journey.api.repository.security.ConfirmationCodeRepository;
7+
import com.github.nramc.dev.journey.api.web.resources.rest.users.security.email.UserSecurityEmailAddressAttributeService;
78
import org.junit.jupiter.api.BeforeEach;
89
import org.junit.jupiter.api.Test;
910
import org.junit.jupiter.api.extension.ExtendWith;
1011
import org.mockito.Mock;
1112
import org.mockito.junit.jupiter.MockitoExtension;
1213

1314
import java.time.LocalDateTime;
15+
import java.util.Optional;
1416

1517
import static com.github.nramc.dev.journey.api.services.confirmationcode.ConfirmationUseCase.VERIFY_EMAIL_ADDRESS;
1618
import static com.github.nramc.dev.journey.api.services.email.EmailCodeValidator.EMAIL_CODE_VALIDITY_MINUTES;
1719
import static com.github.nramc.dev.journey.api.web.resources.rest.users.UsersData.AUTH_USER;
20+
import static com.github.nramc.dev.journey.api.web.resources.rest.users.UsersData.EMAIL_ATTRIBUTE;
1821
import static org.assertj.core.api.Assertions.assertThat;
22+
import static org.mockito.ArgumentMatchers.any;
1923
import static org.mockito.Mockito.when;
2024

2125
@ExtendWith(MockitoExtension.class)
@@ -28,22 +32,26 @@ class EmailCodeValidatorTest {
2832
.username(VALID_USER.getUsername())
2933
.type(ConfirmationCodeType.EMAIL_CODE)
3034
.code(VALID_CODE.code())
31-
.receiver(VALID_USER.getEmailAddress())
35+
.receiver(EMAIL_ATTRIBUTE.value())
3236
.useCase(VERIFY_EMAIL_ADDRESS)
3337
.createdAt(LocalDateTime.now())
3438
.build();
3539
@Mock
3640
private ConfirmationCodeRepository codeRepository;
41+
@Mock
42+
private UserSecurityEmailAddressAttributeService emailAddressAttributeService;
3743
private EmailCodeValidator emailCodeValidator;
3844

3945
@BeforeEach
4046
void setup() {
41-
emailCodeValidator = new EmailCodeValidator(codeRepository);
47+
emailCodeValidator = new EmailCodeValidator(codeRepository, emailAddressAttributeService);
4248
}
4349

4450
@Test
4551
void isValid_whenCodeValid_shouldReturnTrue() {
4652
when(codeRepository.findByUsernameAndCode(VALID_USER.getUsername(), VALID_CODE.code())).thenReturn(VALID_CODE_ENTITY);
53+
when(emailAddressAttributeService.provideEmailAttributeIfExists(any(AuthUser.class)))
54+
.thenReturn(Optional.of(EMAIL_ATTRIBUTE));
4755
assertThat(emailCodeValidator.isValid(VALID_CODE, VALID_USER)).isTrue();
4856
}
4957

@@ -59,13 +67,6 @@ void isValid_whenCodeNotActive_shouldReturnFalse() {
5967
assertThat(emailCodeValidator.isValid(VALID_CODE, VALID_USER)).isFalse();
6068
}
6169

62-
@Test
63-
void isValid_whenReceivedEmailAddressNotMatched_shouldReturnFalse() {
64-
when(codeRepository.findByUsernameAndCode(VALID_USER.getUsername(), VALID_CODE.code()))
65-
.thenReturn(VALID_CODE_ENTITY.toBuilder().receiver("[email protected]").build());
66-
assertThat(emailCodeValidator.isValid(VALID_CODE, VALID_USER)).isFalse();
67-
}
68-
6970
@Test
7071
void isValid_whenConfirmationTypeNotMatched_shouldReturnFalse() {
7172
when(codeRepository.findByUsernameAndCode(VALID_USER.getUsername(), VALID_CODE.code()))

0 commit comments

Comments
 (0)