diff --git a/build.gradle b/build.gradle index 7f49b5a..6cde39d 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-mail' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'com.google.cloud:google-cloud-vision:3.12.0' implementation 'com.google.protobuf:protobuf-java:4.28.2' diff --git a/src/main/java/com/cuoco/adapter/in/controller/AuthenticationControllerAdapter.java b/src/main/java/com/cuoco/adapter/in/controller/AuthenticationControllerAdapter.java index 2dcac7d..eeed0ec 100644 --- a/src/main/java/com/cuoco/adapter/in/controller/AuthenticationControllerAdapter.java +++ b/src/main/java/com/cuoco/adapter/in/controller/AuthenticationControllerAdapter.java @@ -7,13 +7,13 @@ import com.cuoco.adapter.in.controller.model.UserPreferencesResponse; import com.cuoco.adapter.in.controller.model.UserRequest; import com.cuoco.adapter.in.controller.model.UserResponse; +import com.cuoco.application.port.in.ActivateUserCommand; import com.cuoco.application.port.in.CreateUserCommand; import com.cuoco.application.port.in.SignInUserCommand; import com.cuoco.application.usecase.model.Allergy; import com.cuoco.application.usecase.model.AuthenticatedUser; import com.cuoco.application.usecase.model.DietaryNeed; import com.cuoco.application.usecase.model.User; -import com.cuoco.application.usecase.model.UserPreferences; import com.cuoco.shared.GlobalExceptionHandler; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -22,32 +22,30 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +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 java.util.List; @Slf4j @RestController +@RequiredArgsConstructor @RequestMapping("/auth") + @Tag(name = "Authentication", description = "Operations related to authenticate users") public class AuthenticationControllerAdapter { private final SignInUserCommand signInUserCommand; private final CreateUserCommand createUserCommand; - - public AuthenticationControllerAdapter( - SignInUserCommand signInUserCommand, - CreateUserCommand createUserCommand - ) { - this.signInUserCommand = signInUserCommand; - this.createUserCommand = createUserCommand; - } + private final ActivateUserCommand activateUserCommand; @PostMapping("/login") @Operation(summary = "POST for user authentication with email and password") @@ -95,14 +93,6 @@ public ResponseEntity login(@RequestBody AuthRequest request) { return ResponseEntity.ok(response); } - private AuthResponse buildAuthResponse(AuthenticatedUser authenticatedUser) { - return AuthResponse.builder() - .data(AuthDataResponse.builder() - .user(buildUserResponse(authenticatedUser.getUser(), authenticatedUser.getToken())) - .build()) - .build(); - } - @PostMapping("/register") @Operation(summary = "POST for user creation with basic data and preferences") @ApiResponses(value = { @@ -143,12 +133,45 @@ public ResponseEntity register(@RequestBody @Valid UserRequest req log.info("Executing POST register with email {}", request.getEmail()); User user = createUserCommand.execute(buildCreateCommand(request)); - UserResponse userResponse = buildUserResponse(user, null); return ResponseEntity.status(HttpStatus.CREATED).body(userResponse); } + @GetMapping("/confirm") + @Operation(summary = "GET for confirm user email") + @ApiResponses(value = { + @ApiResponse( + responseCode = "200", + description = "Email confirmed successfully" + ), + @ApiResponse( + responseCode = "400", + description = "Invalid token or expired", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = GlobalExceptionHandler.ApiErrorResponse.class) + ) + ), + @ApiResponse( + responseCode = "404", + description = "User not found", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = GlobalExceptionHandler.ApiErrorResponse.class) + ) + ) + }) + public ResponseEntity confirmEmail(@RequestParam String token) { + log.info("Executing POST email confirmation for token {}", token); + + ActivateUserCommand.Command command = ActivateUserCommand.Command.builder().token(token).build(); + + activateUserCommand.execute(command); + + return ResponseEntity.ok().build(); + } + private SignInUserCommand.Command buildAuthenticationCommand(AuthRequest request) { return new SignInUserCommand.Command( request.getEmail(), @@ -169,6 +192,14 @@ private CreateUserCommand.Command buildCreateCommand(UserRequest request) { ); } + private AuthResponse buildAuthResponse(AuthenticatedUser authenticatedUser) { + return AuthResponse.builder() + .data(AuthDataResponse.builder() + .user(buildUserResponse(authenticatedUser.getUser(), authenticatedUser.getToken())) + .build()) + .build(); + } + private UserResponse buildUserResponse(User user, String token) { return UserResponse.builder() .id(user.getId()) diff --git a/src/main/java/com/cuoco/adapter/out/email/EmailService.java b/src/main/java/com/cuoco/adapter/out/email/EmailService.java new file mode 100644 index 0000000..754ec09 --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/email/EmailService.java @@ -0,0 +1,6 @@ +package com.cuoco.adapter.out.email; + + +public interface EmailService { + void sendConfirmationEmail(String to, String confirmationLink); +} diff --git a/src/main/java/com/cuoco/adapter/out/email/SendConfirmationNotificationEmailRepositoryAdapter.java b/src/main/java/com/cuoco/adapter/out/email/SendConfirmationNotificationEmailRepositoryAdapter.java new file mode 100644 index 0000000..edfc661 --- /dev/null +++ b/src/main/java/com/cuoco/adapter/out/email/SendConfirmationNotificationEmailRepositoryAdapter.java @@ -0,0 +1,76 @@ +package com.cuoco.adapter.out.email; + +import com.cuoco.adapter.exception.NotAvailableException; +import com.cuoco.application.port.out.SendConfirmationEmailRepository; +import com.cuoco.application.usecase.model.User; +import com.cuoco.shared.model.ErrorDescription; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class SendConfirmationNotificationEmailRepositoryAdapter implements SendConfirmationEmailRepository { + + private final HttpServletRequest request; + private final JavaMailSender mailSender; + + @Override + public void execute(User user, String token) { + try { + log.info("Executing send confirmation email for user with ID {}", user.getId()); + + String confirmationLink = buildConfirmationLink(token); + + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true); + + helper.setFrom("latribudemicalle1480@gmail.com"); + helper.setTo(user.getEmail()); + helper.setSubject("Confirma tu cuenta en Cuoco"); + + String content = """ + + +

¡Bienvenido a Cuoco!

+

Por favor, confirma tu cuenta haciendo clic en el siguiente enlace:

+ Confirmar cuenta +

Si no creaste una cuenta en Cuoco, puedes ignorar este mensaje.

+ + + """.formatted(confirmationLink); + + helper.setText(content, true); + + mailSender.send(message); + + log.info("Successfully sended confirmation email to {} with link {}", user.getEmail(), confirmationLink); + } catch (MessagingException e) { + log.error("Error sending confirmation email to {}: {}", user.getEmail(), e.getMessage()); + throw new NotAvailableException(ErrorDescription.NOT_AVAILABLE.getValue()); + } + } + + private String buildConfirmationLink(String token) { + + String baseUrl = request.getRequestURL().toString() + .replace(request.getRequestURI(), ""); + + String contextPath = request.getContextPath(); + + String confirmationLink = baseUrl + .concat(contextPath) + .concat("/auth/confirm?token=") + .concat(token); + + log.info("Builded URL link from this base {} and this context {}", baseUrl, contextPath); + return confirmationLink; + } +} + diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/UpdateUserDatabaseRepositoryAdapter.java b/src/main/java/com/cuoco/adapter/out/hibernate/UpdateUserDatabaseRepositoryAdapter.java index a9ed2bf..831c41f 100644 --- a/src/main/java/com/cuoco/adapter/out/hibernate/UpdateUserDatabaseRepositoryAdapter.java +++ b/src/main/java/com/cuoco/adapter/out/hibernate/UpdateUserDatabaseRepositoryAdapter.java @@ -64,6 +64,7 @@ private UserHibernateModel buildUserHibernateModel(User user) { .name(user.getName()) .email(user.getEmail()) .password(user.getPassword()) + .active(user.getActive()) .plan(PlanHibernateModel.fromDomain(user.getPlan())) .dietaryNeeds(user.getDietaryNeeds() != null ? user.getDietaryNeeds().stream().map(DietaryNeedHibernateModel::fromDomain).toList() : List.of()) .allergies(user.getAllergies() != null ? user.getAllergies().stream().map(AllergyHibernateModel::fromDomain).toList() : List.of()) diff --git a/src/main/java/com/cuoco/adapter/out/hibernate/model/UserHibernateModel.java b/src/main/java/com/cuoco/adapter/out/hibernate/model/UserHibernateModel.java index ebdce4e..eb09c07 100644 --- a/src/main/java/com/cuoco/adapter/out/hibernate/model/UserHibernateModel.java +++ b/src/main/java/com/cuoco/adapter/out/hibernate/model/UserHibernateModel.java @@ -83,8 +83,8 @@ public User toDomain() { .password(password) .plan(plan.toDomain()) .active(active) - .recipes(recipes.stream().map(RecipeHibernateModel::toDomain).toList()) - .mealPreps(mealPreps.stream().map(MealPrepHibernateModel::toDomain).toList()) + .recipes(recipes != null ? recipes.stream().map(RecipeHibernateModel::toDomain).toList() : null) + .mealPreps(mealPreps != null ? mealPreps.stream().map(MealPrepHibernateModel::toDomain).toList() : null) .createdAt(createdAt) .build(); } diff --git a/src/main/java/com/cuoco/application/port/in/ActivateUserCommand.java b/src/main/java/com/cuoco/application/port/in/ActivateUserCommand.java new file mode 100644 index 0000000..af3e47e --- /dev/null +++ b/src/main/java/com/cuoco/application/port/in/ActivateUserCommand.java @@ -0,0 +1,16 @@ +package com.cuoco.application.port.in; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +public interface ActivateUserCommand { + void execute(Command command); + + @Data + @Builder + @AllArgsConstructor + class Command { + private String token; + } +} diff --git a/src/main/java/com/cuoco/application/port/out/SendConfirmationEmailRepository.java b/src/main/java/com/cuoco/application/port/out/SendConfirmationEmailRepository.java new file mode 100644 index 0000000..141d2bc --- /dev/null +++ b/src/main/java/com/cuoco/application/port/out/SendConfirmationEmailRepository.java @@ -0,0 +1,7 @@ +package com.cuoco.application.port.out; + +import com.cuoco.application.usecase.model.User; + +public interface SendConfirmationEmailRepository { + void execute(User user, String token); +} diff --git a/src/main/java/com/cuoco/application/usecase/ActivateUserUseCase.java b/src/main/java/com/cuoco/application/usecase/ActivateUserUseCase.java new file mode 100644 index 0000000..935c0ef --- /dev/null +++ b/src/main/java/com/cuoco/application/usecase/ActivateUserUseCase.java @@ -0,0 +1,38 @@ +package com.cuoco.application.usecase; + +import com.cuoco.application.port.in.ActivateUserCommand; +import com.cuoco.application.port.out.GetUserByIdRepository; +import com.cuoco.application.port.out.UpdateUserRepository; +import com.cuoco.application.usecase.model.User; +import com.cuoco.application.utils.JwtUtil; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ActivateUserUseCase implements ActivateUserCommand { + + private final GetUserByIdRepository getUserByIdRepository; + private final UpdateUserRepository updateUserRepository; + private final JwtUtil jwtUtil; + + @Override + @Transactional + public void execute(Command command) { + log.info("Executing user activation with token {}", command.getToken()); + + Long id = Long.valueOf(jwtUtil.extractId(command.getToken())); + + User user = getUserByIdRepository.execute(id); + + user.setActive(true); + + updateUserRepository.execute(user); + + log.info("Successfully activated user with ID {} and email {}", user.getId(), user.getEmail()); + } + +} diff --git a/src/main/java/com/cuoco/application/usecase/AuthenticateUserUseCase.java b/src/main/java/com/cuoco/application/usecase/AuthenticateUserUseCase.java index fdfea4e..ef88212 100644 --- a/src/main/java/com/cuoco/application/usecase/AuthenticateUserUseCase.java +++ b/src/main/java/com/cuoco/application/usecase/AuthenticateUserUseCase.java @@ -1,5 +1,6 @@ package com.cuoco.application.usecase; +import com.cuoco.application.exception.ForbiddenException; import com.cuoco.application.exception.UnauthorizedException; import com.cuoco.application.port.in.AuthenticateUserCommand; import com.cuoco.application.port.out.GetUserByEmailRepository; @@ -59,6 +60,11 @@ public AuthenticatedUser execute(Command command) { throw new UnauthorizedException(ErrorDescription.INVALID_CREDENTIALS.getValue()); } + if (user.getActive() != null && !user.getActive()) { + log.info("User with email {} is not activated yet", email); + throw new ForbiddenException(ErrorDescription.USER_NOT_ACTIVATED.getValue()); + } + log.info("User authenticated with email {}", email); return buildAuthenticatedUser(user); } diff --git a/src/main/java/com/cuoco/application/usecase/CreateUserUseCase.java b/src/main/java/com/cuoco/application/usecase/CreateUserUseCase.java index 8002ebb..86de879 100644 --- a/src/main/java/com/cuoco/application/usecase/CreateUserUseCase.java +++ b/src/main/java/com/cuoco/application/usecase/CreateUserUseCase.java @@ -3,12 +3,13 @@ import com.cuoco.application.exception.BadRequestException; import com.cuoco.application.port.in.CreateUserCommand; import com.cuoco.application.port.out.CreateUserRepository; +import com.cuoco.application.port.out.ExistsUserByEmailRepository; import com.cuoco.application.port.out.GetAllergiesByIdRepository; import com.cuoco.application.port.out.GetCookLevelByIdRepository; import com.cuoco.application.port.out.GetDietByIdRepository; import com.cuoco.application.port.out.GetDietaryNeedsByIdRepository; import com.cuoco.application.port.out.GetPlanByIdRepository; -import com.cuoco.application.port.out.ExistsUserByEmailRepository; +import com.cuoco.application.port.out.SendConfirmationEmailRepository; import com.cuoco.application.usecase.model.Allergy; import com.cuoco.application.usecase.model.CookLevel; import com.cuoco.application.usecase.model.Diet; @@ -16,8 +17,11 @@ import com.cuoco.application.usecase.model.Plan; import com.cuoco.application.usecase.model.User; import com.cuoco.application.usecase.model.UserPreferences; +import com.cuoco.application.utils.JwtUtil; import com.cuoco.shared.model.ErrorDescription; +import com.cuoco.shared.utils.PlanConstants; import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @@ -27,6 +31,7 @@ @Slf4j @Component +@RequiredArgsConstructor public class CreateUserUseCase implements CreateUserCommand { private final PasswordEncoder passwordEncoder; @@ -37,26 +42,8 @@ public class CreateUserUseCase implements CreateUserCommand { private final GetCookLevelByIdRepository getCookLevelByIdRepository; private final GetDietaryNeedsByIdRepository getDietaryNeedsByIdRepository; private final GetAllergiesByIdRepository getAllergiesByIdRepository; - - public CreateUserUseCase( - PasswordEncoder passwordEncoder, - CreateUserRepository createUserRepository, - ExistsUserByEmailRepository existsUserByEmailRepository, - GetPlanByIdRepository getPlanByIdRepository, - GetDietByIdRepository getDietByIdRepository, - GetCookLevelByIdRepository getCookLevelByIdRepository, - GetDietaryNeedsByIdRepository getDietaryNeedsByIdRepository, - GetAllergiesByIdRepository getAllergiesByIdRepository - ) { - this.passwordEncoder = passwordEncoder; - this.createUserRepository = createUserRepository; - this.existsUserByEmailRepository = existsUserByEmailRepository; - this.getPlanByIdRepository = getPlanByIdRepository; - this.getDietByIdRepository = getDietByIdRepository; - this.getCookLevelByIdRepository = getCookLevelByIdRepository; - this.getDietaryNeedsByIdRepository = getDietaryNeedsByIdRepository; - this.getAllergiesByIdRepository = getAllergiesByIdRepository; - } + private final SendConfirmationEmailRepository sendConfirmationEmailRepository; + private final JwtUtil jwtUtil; @Transactional public User execute(Command command) { @@ -70,9 +57,9 @@ public User execute(Command command) { List existingNeeds = getDietaryNeeds(command); List existingAlergies = getAllergies(command); - Plan plan = getPlan(command.getPlanId()); - CookLevel cookLevel = getCookLevel(command.getCookLevelId()); - Diet diet = getDiet(command.getDietId()); + Plan plan = getPlanByIdRepository.execute(PlanConstants.FREE.getValue()); + CookLevel cookLevel = getCookLevelByIdRepository.execute(command.getCookLevelId()); + Diet diet = getDietByIdRepository.execute(command.getDietId()); UserPreferences preferencesToSave = buildUserPreferences(cookLevel, diet); User userToSave = buildUser(command, preferencesToSave, plan, existingNeeds, existingAlergies); @@ -81,19 +68,9 @@ public User execute(Command command) { userCreated.setPassword(null); - return userCreated; - } + sendConfirmationEmail(userCreated); - private Plan getPlan(Integer planId) { - return getPlanByIdRepository.execute(planId); - } - - private Diet getDiet(Integer dietId) { - return getDietByIdRepository.execute(dietId); - } - - private CookLevel getCookLevel(Integer cookLevelId) { - return getCookLevelByIdRepository.execute(cookLevelId); + return userCreated; } private List getDietaryNeeds(Command command) { @@ -141,7 +118,7 @@ private User buildUser( .email(command.getEmail()) .password(encriptedPassword) .plan(plan) - .active(true) + .active(false) .preferences(preferences) .dietaryNeeds(existingNeeds) .allergies(existingAlergies) @@ -155,4 +132,8 @@ private UserPreferences buildUserPreferences(CookLevel cookLevel, Diet diet) { .build(); } + private void sendConfirmationEmail(User user) { + String token = jwtUtil.generateActivationToken(user); + sendConfirmationEmailRepository.execute(user, token); + } } diff --git a/src/main/java/com/cuoco/application/usecase/SignInUserUseCase.java b/src/main/java/com/cuoco/application/usecase/SignInUserUseCase.java index 4e8ea0f..6615a01 100644 --- a/src/main/java/com/cuoco/application/usecase/SignInUserUseCase.java +++ b/src/main/java/com/cuoco/application/usecase/SignInUserUseCase.java @@ -5,8 +5,8 @@ import com.cuoco.application.port.out.GetUserByEmailRepository; import com.cuoco.application.usecase.model.AuthenticatedUser; import com.cuoco.application.usecase.model.User; -import com.cuoco.shared.model.ErrorDescription; import com.cuoco.application.utils.JwtUtil; +import com.cuoco.shared.model.ErrorDescription; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; @@ -41,6 +41,11 @@ public AuthenticatedUser execute(Command command) { throw new ForbiddenException(ErrorDescription.INVALID_CREDENTIALS.getValue()); } + if(user.getActive() != null && !user.getActive()) { + log.info("User with email {} is not activated yet", user.getEmail()); + throw new ForbiddenException(ErrorDescription.USER_NOT_ACTIVATED.getValue()); + } + user.setPassword(null); return buildAuthenticatedUser(user); diff --git a/src/main/java/com/cuoco/application/usecase/UpdateUserProfileUseCase.java b/src/main/java/com/cuoco/application/usecase/UpdateUserProfileUseCase.java index e7c5e91..1ef561f 100644 --- a/src/main/java/com/cuoco/application/usecase/UpdateUserProfileUseCase.java +++ b/src/main/java/com/cuoco/application/usecase/UpdateUserProfileUseCase.java @@ -6,7 +6,6 @@ import com.cuoco.application.port.out.GetCookLevelByIdRepository; import com.cuoco.application.port.out.GetDietByIdRepository; import com.cuoco.application.port.out.GetDietaryNeedsByIdRepository; -import com.cuoco.application.port.out.GetPlanByIdRepository; import com.cuoco.application.port.out.GetUserByIdRepository; import com.cuoco.application.port.out.UpdateUserRepository; import com.cuoco.application.usecase.domainservice.UserDomainService; @@ -14,13 +13,11 @@ import com.cuoco.application.usecase.model.CookLevel; import com.cuoco.application.usecase.model.Diet; import com.cuoco.application.usecase.model.DietaryNeed; -import com.cuoco.application.usecase.model.Plan; import com.cuoco.application.usecase.model.User; import com.cuoco.application.usecase.model.UserPreferences; import com.cuoco.shared.model.ErrorDescription; -import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import java.util.Collections; @@ -28,45 +25,25 @@ @Slf4j @Component +@RequiredArgsConstructor public class UpdateUserProfileUseCase implements UpdateUserProfileCommand { private final UserDomainService userDomainService; private final GetUserByIdRepository getUserByIdRepository; private final UpdateUserRepository updateUserRepository; - private final GetPlanByIdRepository getPlanByIdRepository; private final GetDietByIdRepository getDietByIdRepository; private final GetCookLevelByIdRepository getCookLevelByIdRepository; private final GetDietaryNeedsByIdRepository getDietaryNeedsByIdRepository; private final GetAllergiesByIdRepository getAllergiesByIdRepository; - public UpdateUserProfileUseCase( - UserDomainService userDomainService, - GetUserByIdRepository getUserByIdRepository, - UpdateUserRepository updateUserRepository, - GetPlanByIdRepository getPlanByIdRepository, - GetDietByIdRepository getDietByIdRepository, - GetCookLevelByIdRepository getCookLevelByIdRepository, - GetDietaryNeedsByIdRepository getDietaryNeedsByIdRepository, - GetAllergiesByIdRepository getAllergiesByIdRepository - ) { - this.userDomainService = userDomainService; - this.getUserByIdRepository = getUserByIdRepository; - this.updateUserRepository = updateUserRepository; - this.getPlanByIdRepository = getPlanByIdRepository; - this.getDietByIdRepository = getDietByIdRepository; - this.getCookLevelByIdRepository = getCookLevelByIdRepository; - this.getDietaryNeedsByIdRepository = getDietaryNeedsByIdRepository; - this.getAllergiesByIdRepository = getAllergiesByIdRepository; - } - @Override public User execute(Command command) { User user = userDomainService.getCurrentUser(); log.info("Executing update user use case with ID {}", user.getId()); User existingUser = getUserByIdRepository.execute(user.getId()); - User userToUpdate = buildUpdateUser(existingUser, command); + User updatedUser = updateUserRepository.execute(userToUpdate); log.info("User with ID {} updated successfully", user.getId()); @@ -76,14 +53,14 @@ public User execute(Command command) { private User buildUpdateUser(User existingUser, Command command) { String updatedName = command.getName() != null ? command.getName() : existingUser.getName(); - Plan updatedPlan = command.getPlanId() != null ? getPlanByIdRepository.execute(command.getPlanId()) : existingUser.getPlan(); return User.builder() .id(existingUser.getId()) .name(updatedName) .email(existingUser.getEmail()) .password(existingUser.getPassword()) - .plan(updatedPlan) + .active(existingUser.getActive()) + .plan(existingUser.getPlan()) .preferences(buildUserPreferences(existingUser.getPreferences(), command)) .dietaryNeeds(getUpdatedDietaryNeeds(command, existingUser.getDietaryNeeds())) .allergies(getUpdatedAllergies(command, existingUser.getAllergies())) diff --git a/src/main/java/com/cuoco/application/utils/JwtUtil.java b/src/main/java/com/cuoco/application/utils/JwtUtil.java index 82fe5a9..c33b297 100644 --- a/src/main/java/com/cuoco/application/utils/JwtUtil.java +++ b/src/main/java/com/cuoco/application/utils/JwtUtil.java @@ -22,6 +22,7 @@ public class JwtUtil { public String generateToken(User user) { return Jwts.builder() + .setId(user.getId().toString()) .setSubject(user.getEmail()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 horas @@ -29,6 +30,31 @@ public String generateToken(User user) { .compact(); } + public String generateActivationToken(User user) { + return Jwts.builder() + .setId(user.getId().toString()) + .setSubject(user.getEmail()) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 2)) // 2 horas + .signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()), SignatureAlgorithm.HS256) + .compact(); + } + + public String extractId(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes())) + .build() + .parseClaimsJws(token) + .getBody() + .getId(); + + } catch (MalformedJwtException e) { + log.warn("Extract ID: Invalid JWT token: {}", e.getMessage()); + throw new UnauthorizedException(ErrorDescription.INVALID_CREDENTIALS.getValue()); + } + } + public String extractEmail(String token) { try { return Jwts.parserBuilder() @@ -39,7 +65,7 @@ public String extractEmail(String token) { .getSubject(); } catch (MalformedJwtException e) { - log.warn("Invalid JWT token: {}", e.getMessage()); + log.warn("Extract email: Invalid JWT token: {}", e.getMessage()); throw new UnauthorizedException(ErrorDescription.INVALID_CREDENTIALS.getValue()); } } diff --git a/src/main/java/com/cuoco/shared/config/MailConfiguration.java b/src/main/java/com/cuoco/shared/config/MailConfiguration.java new file mode 100644 index 0000000..1e6906a --- /dev/null +++ b/src/main/java/com/cuoco/shared/config/MailConfiguration.java @@ -0,0 +1,45 @@ +package com.cuoco.shared.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.JavaMailSenderImpl; + +import java.util.Properties; + +@Configuration +public class MailConfiguration { + + @Value("${email.host}") + private String host; + + @Value("${email.port}") + private int port; + + @Value("${email.username}") + private String username; + + @Value("${email.password}") + private String password; + + @Bean + public JavaMailSender javaMailSender() { + + JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); + mailSender.setHost(host); + mailSender.setPort(port); + mailSender.setUsername(username); + mailSender.setPassword(password); + + Properties props = mailSender.getJavaMailProperties(); + props.put("mail.transport.protocol", "smtp"); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.smtp.timeout", "5000"); + props.put("mail.smtp.connectiontimeout", "5000"); + props.put("mail.smtp.writetimeout", "5000"); + + return mailSender; + } +} diff --git a/src/main/java/com/cuoco/shared/model/ErrorDescription.java b/src/main/java/com/cuoco/shared/model/ErrorDescription.java index 684d9d5..e14def3 100644 --- a/src/main/java/com/cuoco/shared/model/ErrorDescription.java +++ b/src/main/java/com/cuoco/shared/model/ErrorDescription.java @@ -23,7 +23,9 @@ public enum ErrorDescription { USER_NOT_EXISTS("El usuario ingresado no existe"), USER_DUPLICATED("El usuario ya existe"), + USER_NOT_ACTIVATED("El usuario no esta activo"), NO_AUTH_TOKEN("El token no esta presente"), + INVALID_TOKEN("El token ingresado no es válido"), INVALID_CREDENTIALS("Las credenciales no son válidas"), EXPIRED_CREDENTIALS("El token ha expirado"), diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 59fe491..d961a1e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,6 +26,20 @@ gemini: image: url: ${GEMINI_IMAGE_URL} temperature: ${GEMINI_TEMPERATURE} +email: + host: ${EMAIL_HOST:smtp.gmail.com} + port: ${EMAIL_PORT:587} + username: ${EMAIL_USERNAME:cuoco.8bits@gmail.com} + password: ${EMAIL_PASSWORD} + properties: + mail: + smtp: + auth: true + starttls: + enable: true + connectiontimeout: 5000 + timeout: 5000 + writetimeout: 5000 shared: recipes: size: