Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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")
Expand Down Expand Up @@ -95,14 +93,6 @@ public ResponseEntity<AuthResponse> 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 = {
Expand Down Expand Up @@ -143,12 +133,45 @@ public ResponseEntity<UserResponse> 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(),
Expand All @@ -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())
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/com/cuoco/adapter/out/email/EmailService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.cuoco.adapter.out.email;


public interface EmailService {
void sendConfirmationEmail(String to, String confirmationLink);
}
Original file line number Diff line number Diff line change
@@ -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 = """
<html>
<body>
<h2>¡Bienvenido a Cuoco!</h2>
<p>Por favor, confirma tu cuenta haciendo clic en el siguiente enlace:</p>
<a href="%s">Confirmar cuenta</a>
<p>Si no creaste una cuenta en Cuoco, puedes ignorar este mensaje.</p>
</body>
</html>
""".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;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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());
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Loading