Skip to content
Draft
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
4 changes: 3 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ val quarkusPlatformVersion: String by project
dependencies {
errorprone(libs.errorprone.core)
errorprone(libs.nullaway)

implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))

implementation(libs.quarkus.arc)
Expand All @@ -33,6 +33,8 @@ dependencies {
implementation(libs.quarkus.rest)
implementation(libs.quarkus.rest.jackson)
implementation(libs.quarkus.smallrye.jwt)
implementation("io.quarkiverse.amazonservices:quarkus-amazon-s3:3.12.1")
implementation("software.amazon.awssdk:url-connection-client:2.40.13")

testImplementation(libs.assertj.core)
testImplementation(libs.mockito.junit)
Expand Down
Binary file added app/data/images/spaghetti.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
package dev.blaauwendraad.recipe_book.config;

import dev.blaauwendraad.recipe_book.service.ImageService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.io.File;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.flywaydb.core.Flyway;
import org.jboss.logging.Logger;

@ApplicationScoped
public final class DemoDataSyncService {
private static final Logger log = Logger.getLogger(DemoDataSyncService.class);
private ImageService imageService;

private final String jdbcUrl;
private final String username;
private final String password;

@Inject
public DemoDataSyncService(
@ConfigProperty(name = "quarkus.datasource.jdbc.url") String jdbcUrl,
@ConfigProperty(name = "quarkus.datasource.username") String username,
@ConfigProperty(name = "quarkus.datasource.password") String password) {
@ConfigProperty(name = "quarkus.datasource.password") String password,
ImageService imageService) {
this.jdbcUrl = jdbcUrl;
this.username = username;
this.password = password;
this.imageService = imageService;
}

@Transactional
Expand All @@ -46,4 +53,10 @@ void insertDemoData() {
throw new RuntimeException("Demo data migration failed", e);
}
}

void insertDemoImage() {
File imageFile = new File("data/images/spaghetti.jpg");
imageService.putObject(imageFile);
log.info("Inserted demo image into Garage/S3");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public void syncReferenceData(@Observes StartupEvent startupEvent) {
validateRoles();
if (profile.isPresent() && "dev".equals(profile.get())) {
demoDataSyncService.insertDemoData();
demoDataSyncService.insertDemoImage();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public class RecipeEntity extends PanacheEntityBase {
@Nullable
public String description;

@Column(name = "image_name")
@Nullable
public String imageName;

@Column(name = "num_servings")
@SuppressWarnings("NullAway.Init")
public Integer numServings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public Long persistRecipeEntity(
UserAccountEntity userAccountEntity,
String title,
@Nullable String description,
@Nullable String imageName,
Integer numServings,
PreparationTime preparationTime,
List<Ingredient> ingredients,
Expand All @@ -49,6 +50,7 @@ public Long persistRecipeEntity(
var recipeEntity = existingRecipeEntity != null ? existingRecipeEntity : new RecipeEntity();
recipeEntity.title = title;
recipeEntity.description = description;
recipeEntity.imageName = imageName;
recipeEntity.numServings = numServings;
recipeEntity.preparationTime = preparationTime;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package dev.blaauwendraad.recipe_book.service;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import java.io.File;
import software.amazon.awssdk.core.ResponseBytes;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

@ApplicationScoped
public class ImageService {
private static final String BUCKET_NAME = "images";
private S3Client s3Client;

@Inject
public ImageService(S3Client s3Client) {
this.s3Client = s3Client;
}

public void putObject(File file) {
PutObjectRequest request = PutObjectRequest.builder()
.bucket(BUCKET_NAME)
.key(file.getName())
.contentType("image/jpeg")
.build();
s3Client.putObject(request, RequestBody.fromFile(file));
}

public byte[] getObject(String objectKey) {
GetObjectRequest getObjectRequest =
GetObjectRequest.builder().bucket(BUCKET_NAME).key(objectKey).build();
ResponseBytes<GetObjectResponse> objectBytes = s3Client.getObjectAsBytes(getObjectRequest);
return objectBytes.asByteArray();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ private RecipeSummary toRecipeSummary(RecipeEntity recipeEntity) {
recipeEntity.id,
recipeEntity.title,
recipeEntity.description,
recipeEntity.imageName,
recipeEntity.numServings,
recipeEntity.preparationTime,
recipeEntity.author == null ? null : new Author(recipeEntity.author.id, recipeEntity.author.username));
Expand All @@ -77,6 +78,7 @@ public Recipe getRecipeById(Long recipeId) {
recipeEntity.id,
recipeEntity.title,
recipeEntity.description,
recipeEntity.imageName,
recipeEntity.numServings,
recipeEntity.preparationTime,
recipeEntity.author == null ? null : new Author(recipeEntity.author.id, recipeEntity.author.username),
Expand All @@ -92,6 +94,7 @@ public Recipe getRecipeById(Long recipeId) {
public Long createRecipe(
String title,
@Nullable String description,
@Nullable String imageName,
Integer numServings,
PreparationTime preparationTime,
Long userId,
Expand All @@ -106,6 +109,7 @@ public Long createRecipe(
userAccountEntity,
title,
description,
imageName,
numServings,
preparationTime,
ingredients,
Expand Down Expand Up @@ -134,6 +138,7 @@ public void updateRecipe(
Long recipeId,
String title,
@Nullable String description,
@Nullable String imageName,
Integer numServings,
PreparationTime preparationTime,
Long userId,
Expand All @@ -156,6 +161,7 @@ public void updateRecipe(
existingRecipeEntity.author,
title,
description,
imageName,
numServings,
preparationTime,
ingredients,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import dev.blaauwendraad.recipe_book.repository.RefreshTokenRepository;
import dev.blaauwendraad.recipe_book.repository.UserRepository;
import dev.blaauwendraad.recipe_book.service.exception.AccessTokenRefreshException;
import dev.blaauwendraad.recipe_book.service.exception.UserLoginException;
import dev.blaauwendraad.recipe_book.service.exception.UserAuthenticationException;
import dev.blaauwendraad.recipe_book.service.exception.UserRegistrationException;
import dev.blaauwendraad.recipe_book.service.exception.UserRegistrationValidationException;
import dev.blaauwendraad.recipe_book.service.model.AuthenticationDetails;
Expand Down Expand Up @@ -63,9 +63,9 @@ public UserAccount registerUser(String username, String emailAddress, String pas
*
* @param emailAddress the e-mail address of the user
* @param password the password of the user
* @throws UserLoginException if there is an error during user login
* @throws UserAuthenticationException if there is an error during user login
*/
public AuthenticationDetails login(String emailAddress, String password) throws UserLoginException {
public AuthenticationDetails login(String emailAddress, String password) throws UserAuthenticationException {
UserAccount userAccount = validateCredentials(emailAddress, password);
RefreshTokenEntity refreshTokenEntity = createAndStoreRefreshToken(userAccount);
return new AuthenticationDetails(
Expand Down Expand Up @@ -123,13 +123,13 @@ private String generateRefreshToken() {
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
}

private UserAccount validateCredentials(String emailAddress, String password) throws UserLoginException {
private UserAccount validateCredentials(String emailAddress, String password) throws UserAuthenticationException {
UserAccountEntity userAccountEntity = userRepository.findByEmail(emailAddress);
if (userAccountEntity == null) {
throw new UserLoginException("No user account found for the provided email address.");
throw new UserAuthenticationException("No user account found for the provided email address.");
}
if (!BcryptUtil.matches(password, userAccountEntity.passwordHash)) {
throw new UserLoginException("Invalid password provided.");
throw new UserAuthenticationException("Invalid password provided.");
}
return UserAccountConverter.toUserAccount(userAccountEntity);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package dev.blaauwendraad.recipe_book.service;

import dev.blaauwendraad.recipe_book.data.model.UserAccountEntity;
import dev.blaauwendraad.recipe_book.repository.UserRepository;
import dev.blaauwendraad.recipe_book.service.exception.UserAuthenticationException;
import io.quarkus.elytron.security.common.BcryptUtil;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;

@ApplicationScoped
public class UserService {
private final UserRepository userRepository;

@Inject
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}

public String getEmail(Long userId) {
UserAccountEntity userAccountEntity = userRepository.findById(userId);
if (userAccountEntity == null) {
throw new NotFoundException("User with userId " + userId + " does not exist");
}
return userAccountEntity.emailAddress;
}

@Transactional
public void updateEmail(Long userId, String newEmail, String currentPassword) throws UserAuthenticationException {
UserAccountEntity userAccountEntity = userRepository.findById(userId);
if (userAccountEntity == null) {
throw new UserAuthenticationException("No user account found for the provided user ID.");
}
if (!BcryptUtil.matches(currentPassword, userAccountEntity.passwordHash)) {
throw new UserAuthenticationException("Invalid password provided.");
}
userAccountEntity.emailAddress = newEmail;
userRepository.persist(userAccountEntity);
}

@Transactional
public void updatePassword(Long userId, String currentPassword, String newPassword, String confirmPassword)
throws UserAuthenticationException {
UserAccountEntity userAccountEntity = userRepository.findById(userId);
if (userAccountEntity == null) {
throw new UserAuthenticationException("No user account found for the provided user ID.");
}
if (!BcryptUtil.matches(currentPassword, userAccountEntity.passwordHash)) {
throw new UserAuthenticationException("Invalid password provided.");
}
if (!newPassword.equals(confirmPassword)) {
throw new UserAuthenticationException("New password and confirmation password do not match.");
}
userAccountEntity.passwordHash = BcryptUtil.bcryptHash(newPassword);
userRepository.persist(userAccountEntity);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package dev.blaauwendraad.recipe_book.service.exception;

public class UserAuthenticationException extends DetailedMessageException {

public UserAuthenticationException(String detailMessage) {
super("Failed to login user.", detailMessage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,22 @@
import jakarta.ws.rs.ext.Provider;

@Provider
public class UserLoginExceptionMapper implements ExceptionMapper<UserLoginException> {
public class UserAuthenticationExceptionMapper implements ExceptionMapper<UserAuthenticationException> {

@Override
public Response toResponse(UserLoginException exception) {
public Response toResponse(UserAuthenticationException exception) {
var errorResponse = toErrorResponse(exception);
return Response.status(errorResponse.httpStatusCode())
.type(MediaType.APPLICATION_JSON_TYPE)
.entity(errorResponse)
.build();
}

private static ErrorResponse toErrorResponse(UserLoginException exception) {
private static ErrorResponse toErrorResponse(UserAuthenticationException exception) {
return new ErrorResponse(
exception.getMessage() != null
? exception.getMessage()
: "An unexpected error occurred while trying to log in",
: "An unexpected error occurred while trying to authenticate the user.",
exception.getDetailMessage(),
Response.Status.BAD_REQUEST.getStatusCode());
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public record Recipe(
Long id,
String title,
@Nullable String description,
@Nullable String imageName,
Integer numServings,
PreparationTime preparationTime,
@Nullable Author author,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public record RecipeSummary(
Long id,
String title,
@Nullable String description,
@Nullable String imageName,
Integer numServings,
PreparationTime preparationTime,
@Nullable Author author) {}
Loading