diff --git a/.github/workflows/backend_test.yml b/.github/workflows/backend_test.yml index 6d33851e..7fbcc1f8 100644 --- a/.github/workflows/backend_test.yml +++ b/.github/workflows/backend_test.yml @@ -3,30 +3,23 @@ name: backend-test on: push: paths: - - 'backend/**' + - "backend/**" - compose.yml - dev.yml - - '.github/workflows/backend_test.yml' - + - ".github/workflows/backend_test.yml" pull_request: branches: - main - staging - - develop - jobs: test: runs-on: ubuntu-latest env: - working-directory: - backend + working-directory: backend steps: - name: Checkout uses: actions/checkout@v4 - + - name: Test with Maven run: docker compose -f dev.yml run --rm backend mvn test - - - diff --git a/.github/workflows/frontend_test.yml b/.github/workflows/frontend_test.yml index b57d72a0..79b24a57 100644 --- a/.github/workflows/frontend_test.yml +++ b/.github/workflows/frontend_test.yml @@ -1,6 +1,14 @@ name: Front-End CI -on: [push, pull_request] +on: + push: + paths: + - "frontend/**" + - ".github/workflows/frontend_test.yml" + pull_request: + branches: + - main + - staging jobs: build: @@ -15,8 +23,8 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v2 with: - node-version: '20' - cache: 'yarn' + node-version: "20" + cache: "yarn" - name: Enable Corepack run: corepack enable diff --git a/backend/pom.xml b/backend/pom.xml index 470caae3..81791029 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -21,6 +21,18 @@ org.springframework.boot spring-boot-starter-security + + org.mockito + mockito-core + 5.2.0 + test + + + org.mockito + mockito-junit-jupiter + 5.2.0 + test + org.apache.jena apache-jena-libs diff --git a/backend/src/main/java/com/group1/cuisines/config/SecurityConfiguration.java b/backend/src/main/java/com/group1/cuisines/config/SecurityConfiguration.java index 653b2358..7ee00e81 100644 --- a/backend/src/main/java/com/group1/cuisines/config/SecurityConfiguration.java +++ b/backend/src/main/java/com/group1/cuisines/config/SecurityConfiguration.java @@ -45,6 +45,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) .hasRole("ADMIN") // Require ADMIN role for "/api/v1/resources" .requestMatchers(HttpMethod.POST,"/**") .authenticated() + .requestMatchers("/feed?type=following").authenticated() .requestMatchers(HttpMethod.DELETE,"/**").authenticated()) // Require authentication for all other requests .sessionManagement( manager -> diff --git a/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java b/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java index 0ffe909b..777b70ad 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java @@ -6,6 +6,7 @@ import com.group1.cuisines.dao.response.AuthenticationTokenResponse; import com.group1.cuisines.services.AuthenticationService; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -23,13 +24,27 @@ public class AuthenticationController { public ResponseEntity> signup( @RequestBody SignUpRequest request ) { - return ResponseEntity.ok(authenticationService.signup(request)); // Return response + ApiResponse response = authenticationService.signup(request); + + if (response.getStatus() == 409) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(response); + } else if (response.getStatus() == 400) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } else { + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } } @PostMapping("/login") // Sign in endpoint public ResponseEntity> signin( @RequestBody SignInRequest request ) { - return ResponseEntity.ok(authenticationService.signin(request)); // Return response + ApiResponse response = authenticationService.signin(request); + + if (response.getStatus() == 401) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); + } else { + return ResponseEntity.ok(response); + } } } diff --git a/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java b/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java index 996df87f..eadf6d76 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java @@ -1,11 +1,16 @@ package com.group1.cuisines.controllers; import com.group1.cuisines.dao.response.ApiResponse; +import com.group1.cuisines.dao.response.ErrorResponse; +import com.group1.cuisines.dao.response.SuccessResponse; +import com.group1.cuisines.dto.CuisineDetailsDto; import com.group1.cuisines.entities.Cuisine; import com.group1.cuisines.repositories.CuisineRepository; import com.group1.cuisines.services.CuisineService; +import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -21,14 +26,12 @@ public class CuisineController { private final CuisineRepository cuisineRepository; @GetMapping("/{cuisineId}") - public ResponseEntity getCuisineDetails( - @PathVariable String cuisineId, - @RequestParam(defaultValue = "false") boolean includeDishes) { - - Cuisine cuisine = cuisineService.getCuisineById(cuisineId, includeDishes); - if (cuisine == null) { - return ResponseEntity.notFound().build(); + public ResponseEntity getCuisineById(@PathVariable String cuisineId, @RequestParam(required = false) Boolean includeDishes) { + try { + CuisineDetailsDto cuisineDetails = cuisineService.getCuisineById(cuisineId, Boolean.TRUE.equals(includeDishes)); + return ResponseEntity.ok(new SuccessResponse<>(200,cuisineDetails, "Cuisine details fetched successfully")); + } catch (EntityNotFoundException e) { + return ResponseEntity.ok(new ErrorResponse(204,"Cuisine not found")); } - return ResponseEntity.ok(cuisine); } } diff --git a/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java b/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java new file mode 100644 index 00000000..d4fdc09b --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java @@ -0,0 +1,45 @@ +package com.group1.cuisines.controllers; + +import com.group1.cuisines.dao.response.SuccessResponse; +import com.group1.cuisines.dto.RecipeDetailsDto; +import com.group1.cuisines.services.RecipeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; + +@RestController +@RequestMapping("/api/v1") +public class FeedController { + + @Autowired + private RecipeService recipeService; + + @GetMapping("/feed") + public ResponseEntity getFeed(@RequestParam String type, Authentication authentication) { + if (!"explore".equals(type) && !"following".equals(type)) { + return ResponseEntity.badRequest().body("Invalid type parameter."); + } + + if ("following".equals(type)) { + if (authentication == null || !authentication.isAuthenticated()) { + // Return an empty set and a message for unauthenticated users + return ResponseEntity.ok(new SuccessResponse<>(400,Collections.emptyList(), "No content available. Please log in and follow other users !.")); + } + // Fetch following users' recipes for authenticated users + String username = authentication.getName(); + List recipes = recipeService.getRecipesByType(type, username); + return ResponseEntity.ok(new SuccessResponse<>(200,recipes, "Recipes fetched successfully from followed users.")); + } + + // For 'explore', accessible to everyone + List recipes = recipeService.getRecipesByType(type, null); + return ResponseEntity.ok(new SuccessResponse<>(200,recipes, "Recipes fetched successfully.")); + } +} diff --git a/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java b/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java index 71804121..814ec4d0 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java @@ -1,8 +1,6 @@ package com.group1.cuisines.controllers; -import com.group1.cuisines.dto.CommentsDto; -import com.group1.cuisines.dto.NewRecipeDto; -import com.group1.cuisines.dto.RatingDto; -import com.group1.cuisines.dto.RecipeDetailDto; +import com.group1.cuisines.dao.response.SuccessResponse; +import com.group1.cuisines.dto.*; import com.group1.cuisines.entities.Comment; import com.group1.cuisines.entities.User; import com.group1.cuisines.services.RecipeService; @@ -23,6 +21,28 @@ public class RecipeController { @Autowired private RecipeService recipeService; + + @GetMapping("/recipes/{recipeId}") + public ResponseEntity getRecipeById(@PathVariable Integer recipeId) { + RecipeDetailsDto recipeDetails = recipeService.getRecipeById(recipeId); + if (recipeDetails != null) { + return ResponseEntity.ok(new SuccessResponse<>(200,recipeDetails, "Recipe fetched successfully")); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Recipe not found"); + } + } + + + @GetMapping("/recipes") + public ResponseEntity> getRecipes(@RequestParam(required = false) String sort, + @RequestParam(required = false) String dishId, + @RequestParam(required = false) String cuisineId) { + List recipes = recipeService.findRecipes(sort, dishId, cuisineId); + return ResponseEntity.ok(recipes); + } + + + @PostMapping("/recipes") public ResponseEntity createRecipe(@RequestBody NewRecipeDto newRecipe) throws Exception{ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); diff --git a/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java b/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java index 05799f48..1d53a486 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java @@ -1,6 +1,10 @@ package com.group1.cuisines.controllers; import com.group1.cuisines.dao.response.ApiResponse; +import com.group1.cuisines.dao.response.ErrorResponse; +import com.group1.cuisines.dao.response.SuccessResponse; +import com.group1.cuisines.dto.DishResponseDto; +import com.group1.cuisines.dto.UserDto; import com.group1.cuisines.entities.Dish; import com.group1.cuisines.entities.User; import com.group1.cuisines.services.SearchService; @@ -8,6 +12,7 @@ import com.group1.cuisines.services.WikidataService; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -28,15 +33,24 @@ public ResponseEntity searchUsers(@RequestParam(required = false) String q) { List users = userService.searchUsers(q); if (users.isEmpty()) { // Return a custom message with a "No Content" status when no users are found - return ResponseEntity.status(HttpStatus.NO_CONTENT).body("No users found"); + return ResponseEntity.ok(new ErrorResponse(204,"No users found")); } - return ResponseEntity.ok(users); // Return the list of users when found + return ResponseEntity.ok(new SuccessResponse<>(200, users.stream().map(user -> new UserDto( + user.getId(), + user.getUsername(), + user.getFirstName(), + user.getLastName(), + user.getFollowerCount(), + user.getFollowingCount(), + user.getRecipeCount() + )).collect(Collectors.toList()), "Users fetched successfully")); + } @GetMapping("/dishes") - public ApiResponse> searchDishes(@RequestParam(required = false) String q, - @RequestParam(required = false) String cuisine, - @RequestParam(required = false) String foodType) { + public ApiResponse> searchDishes(@RequestParam(required = false) String q, + @RequestParam(required = false) String cuisine, + @RequestParam(required = false) String foodType) { return new ApiResponse<>( 200, "Search completed", diff --git a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java index c5a9d711..a7b6a138 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java @@ -1,12 +1,17 @@ package com.group1.cuisines.controllers; +import com.group1.cuisines.dao.response.ErrorResponse; +import com.group1.cuisines.dao.response.SuccessResponse; import com.group1.cuisines.dto.UserDto; +import com.group1.cuisines.dto.UserProfileDto; import com.group1.cuisines.entities.User; import com.group1.cuisines.repositories.UserRepository; import com.group1.cuisines.services.UserService; +import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; @@ -25,6 +30,18 @@ public class UserController { private final UserService userService; private final UserRepository userRepository; + + @GetMapping("/{userId}") + public ResponseEntity getUserById(@PathVariable Integer userId, Authentication authentication) { + String currentUsername = authentication != null ? authentication.getName() : null; + try { + UserProfileDto userProfile = userService.getUserProfileById(userId, currentUsername); + return ResponseEntity.ok(new SuccessResponse<>(200,userProfile, "User profile fetched successfully")); + } catch (EntityNotFoundException e) { + return ResponseEntity.ok(new ErrorResponse(204,"User not found")); + } + } + @GetMapping("/me") public ResponseEntity getUserDetails(@AuthenticationPrincipal UserDetails userDetails) { if (userDetails instanceof com.group1.cuisines.entities.User) { @@ -41,38 +58,47 @@ public ResponseEntity getUserDetails(@AuthenticationPrincipal UserDetails use return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated"); } - @PostMapping("/{userId}/follow") - public ResponseEntity followUser(@PathVariable Integer userId) { + @PostMapping("/follow") + public ResponseEntity followUser(@RequestBody Map requestBody) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(authentication.getPrincipal()=="anonymousUser"){ - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Authentication required."); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(401,"Authentication required") ); + } String username = authentication.getName(); Integer followerId = userRepository.findUserIdByUsername(username); + Integer userId = requestBody.get("userId"); if (followerId == null || userId == null) { - return ResponseEntity.badRequest().body("Invalid user data"); + + return ResponseEntity.ok(new ErrorResponse(204,"Invalid user data")); } boolean result = userService.followUser(userId, followerId); if (!result) { - return ResponseEntity.status(HttpStatus.ALREADY_REPORTED).body("Already following"); + + return ResponseEntity.ok(new ErrorResponse(209,"Already following")); } - return ResponseEntity.ok().body("Followed successfully"); + + return ResponseEntity.ok(new SuccessResponse<>(200,null,"Followed successfully")); } @GetMapping("/{userId}/following") public ResponseEntity getUserFollowing(@PathVariable Integer userId) { // Validate the provided user ID if (userId == null) { - return ResponseEntity.badRequest().body("Invalid user ID provided"); + + return ResponseEntity.ok(new ErrorResponse(204,"Invalid user ID provided") ); + } + if(userRepository.findById(userId).isEmpty()){ + return ResponseEntity.ok(new ErrorResponse(204,"User not found") ); } Set following = userService.getUserFollowing(userId); if (following.isEmpty()) { - return ResponseEntity.ok().body("User is not following anyone"); + return ResponseEntity.ok(new ErrorResponse(204,"User is not following anyone")); } else { Set followingDto = following.stream() .map(user -> UserDto.builder() @@ -85,18 +111,22 @@ public ResponseEntity getUserFollowing(@PathVariable Integer userId) { .recipeCount(user.getRecipeCount()) .build()) .collect(Collectors.toSet()); - return ResponseEntity.ok().body(followingDto); + return ResponseEntity.ok(new SuccessResponse<>(200,followingDto, "User following fetched successfully")); } } @GetMapping("/{userId}/followers") public ResponseEntity getUserFollowers(@PathVariable Integer userId) { // Validate the provided user ID if (userId == null) { - return ResponseEntity.badRequest().body("Invalid user ID provided"); + + return ResponseEntity.ok(new ErrorResponse(204,"Invalid user ID provided") ); + } + if(userRepository.findById(userId).isEmpty()){ + return ResponseEntity.ok(new ErrorResponse(204,"User not found") ); } Set followers = userService.getUserFollower(userId); if (followers.isEmpty()) { - return ResponseEntity.ok().body("User is not followed by anyone"); + return ResponseEntity.ok(new ErrorResponse(204,"User is not followed by anyone")); } else { Set followingDto = followers.stream() .map(user -> UserDto.builder() @@ -109,29 +139,31 @@ public ResponseEntity getUserFollowers(@PathVariable Integer userId) { .recipeCount(user.getRecipeCount()) .build()) .collect(Collectors.toSet()); - return ResponseEntity.ok().body(followingDto); + return ResponseEntity.ok(new SuccessResponse<>(200,followingDto, "User followers fetched successfully")); } } - @PostMapping("/{userId}/unfollow") - public ResponseEntity unfollowUser(@PathVariable Integer userId) { + @PostMapping("/unfollow") + public ResponseEntity unfollowUser(@RequestBody Map requestBody) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(authentication.getPrincipal()=="anonymousUser"){ - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Authentication required."); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(401,"Authentication required") ); } String username = authentication.getName(); Integer followerId = userRepository.findUserIdByUsername(username); + Integer userId = requestBody.get("userId"); if (followerId == null || userId == null) { - return ResponseEntity.badRequest().body("Invalid user data."); + return ResponseEntity.ok(new ErrorResponse(204,"Invalid user data")); } boolean result = userService.unfollowUser(userId, followerId); if (!result) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Follow relationship does not exist."); + + return ResponseEntity.ok(new ErrorResponse(209,"Follow relationship does not exist")); } - return ResponseEntity.ok().body("Unfollowed successfully."); + return ResponseEntity.ok(new SuccessResponse<>(200,null,"Unfollowed successfully")); } } diff --git a/backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java b/backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java new file mode 100644 index 00000000..df3e3fca --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java @@ -0,0 +1,12 @@ +package com.group1.cuisines.dao.response; +import lombok.*; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ErrorResponse { + private int status; + private String message; +} diff --git a/backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java b/backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java new file mode 100644 index 00000000..b4ab374e --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java @@ -0,0 +1,10 @@ +package com.group1.cuisines.dao.response; +import lombok.*; +@Data +@AllArgsConstructor +public class SuccessResponse { + private int status; + private T data; + private String message; + +} diff --git a/backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java b/backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java new file mode 100644 index 00000000..e22aeb60 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java @@ -0,0 +1,17 @@ +package com.group1.cuisines.dto; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AuthorDto { + private Integer id; + private String name; + private String username; + private Integer followersCount; + private Integer followingCount; + private Integer recipesCount; + +} diff --git a/backend/src/main/java/com/group1/cuisines/dto/BookmarkDto.java b/backend/src/main/java/com/group1/cuisines/dto/BookmarkDto.java new file mode 100644 index 00000000..2f920dae --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/BookmarkDto.java @@ -0,0 +1,21 @@ +package com.group1.cuisines.dto; +import lombok.*; +import java.util.List; +@Data +@NoArgsConstructor +@AllArgsConstructor +public class BookmarkDto { + + private Integer id; + private String name; + private String instructions; + private List ingredients; + private int servingSize; + private Integer cookTime; + //private List images; + private CuisineDto cuisine; + private DishDto dish; + private Double avgRating; + + private AuthorDto author; +} diff --git a/backend/src/main/java/com/group1/cuisines/dto/CuisineDetailsDto.java b/backend/src/main/java/com/group1/cuisines/dto/CuisineDetailsDto.java new file mode 100644 index 00000000..e06a70d0 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/CuisineDetailsDto.java @@ -0,0 +1,15 @@ +package com.group1.cuisines.dto; +import lombok.*; + +import java.util.List; +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CuisineDetailsDto { + private String id; + private String name; + // private String description; + //private String image; + // private Boolean isSelfFollowing; + private List dishes; +} diff --git a/backend/src/main/java/com/group1/cuisines/dto/CuisineDto.java b/backend/src/main/java/com/group1/cuisines/dto/CuisineDto.java new file mode 100644 index 00000000..efdd9272 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/CuisineDto.java @@ -0,0 +1,13 @@ +package com.group1.cuisines.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CuisineDto { + private String id; + private String name; +} \ No newline at end of file diff --git a/backend/src/main/java/com/group1/cuisines/dto/DishDto.java b/backend/src/main/java/com/group1/cuisines/dto/DishDto.java new file mode 100644 index 00000000..93879540 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/DishDto.java @@ -0,0 +1,10 @@ +package com.group1.cuisines.dto; +import lombok.*; +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DishDto { + private String id; + private String name; + private String image; +} \ No newline at end of file diff --git a/backend/src/main/java/com/group1/cuisines/dto/DishResponseDto.java b/backend/src/main/java/com/group1/cuisines/dto/DishResponseDto.java new file mode 100644 index 00000000..6eb4915e --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/DishResponseDto.java @@ -0,0 +1,19 @@ +package com.group1.cuisines.dto; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DishResponseDto { + private String id; + private String name; + private String image; + private String description; + private String countries; + private String ingredients; + private String foodTypes; + private String cuisines; +} diff --git a/backend/src/main/java/com/group1/cuisines/dto/NewRecipeDto.java b/backend/src/main/java/com/group1/cuisines/dto/NewRecipeDto.java index 1bc7faec..cc4c853a 100644 --- a/backend/src/main/java/com/group1/cuisines/dto/NewRecipeDto.java +++ b/backend/src/main/java/com/group1/cuisines/dto/NewRecipeDto.java @@ -14,5 +14,5 @@ public class NewRecipeDto { private int cookingTime; private int servingSize; private String dishId; - private List ingredients; // Nested DTOs for ingredients + private List ingredients; } diff --git a/backend/src/main/java/com/group1/cuisines/dto/RecipeDetailsDto.java b/backend/src/main/java/com/group1/cuisines/dto/RecipeDetailsDto.java new file mode 100644 index 00000000..5f5ec185 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/RecipeDetailsDto.java @@ -0,0 +1,29 @@ +package com.group1.cuisines.dto; + +import java.util.List; + +import com.group1.cuisines.entities.Ingredient; +import lombok.*; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RecipeDetailsDto { + private Integer id; + private String name; + // private String description; + private String instructions; + private List ingredients; + //private List images; + private Integer cookTime; + private Integer servingSize; + // private List allergens; + private CuisineDto cuisine; + private DishDto dish; + private Double avgRating; + + private AuthorDto author; + + + +} diff --git a/backend/src/main/java/com/group1/cuisines/dto/RecipeDto.java b/backend/src/main/java/com/group1/cuisines/dto/RecipeDto.java new file mode 100644 index 00000000..bca92e79 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/RecipeDto.java @@ -0,0 +1,23 @@ +package com.group1.cuisines.dto; + +import lombok.*; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RecipeDto { + private Integer id; + private String title; + private String instructions; + private int preparationTime; + private int cookingTime; + private int servingSize; + private double averageRating; + private String dishName; + + + + +} diff --git a/backend/src/main/java/com/group1/cuisines/dto/UserProfileDto.java b/backend/src/main/java/com/group1/cuisines/dto/UserProfileDto.java new file mode 100644 index 00000000..447263f8 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/UserProfileDto.java @@ -0,0 +1,24 @@ +package com.group1.cuisines.dto; +import lombok.*; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor + + +public class UserProfileDto { + private Integer id; + private String username; + private String name; + private String bio; + private Integer followersCount; + private Integer followingCount; + //private String gender; + // private String profilePicture; + // private List diets; + private Integer recipeCount; + private List bookmarks; + private List recipes; +} diff --git a/backend/src/main/java/com/group1/cuisines/entities/Cuisine.java b/backend/src/main/java/com/group1/cuisines/entities/Cuisine.java index 020aa4c3..6adc193a 100644 --- a/backend/src/main/java/com/group1/cuisines/entities/Cuisine.java +++ b/backend/src/main/java/com/group1/cuisines/entities/Cuisine.java @@ -9,7 +9,9 @@ import java.net.URL; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Data @@ -32,8 +34,20 @@ public class Cuisine { inverseJoinColumns = @JoinColumn(name = "dish_id") ) - private List dishes = new ArrayList<>(); + private Set dishes = new HashSet<>(); + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Cuisine cuisine = (Cuisine) o; + return id != null && id.equals(cuisine.id); + } + + @Override + public int hashCode() { + return 31; + } @Override public String toString() { return "Cuisine{" + diff --git a/backend/src/main/java/com/group1/cuisines/entities/Dish.java b/backend/src/main/java/com/group1/cuisines/entities/Dish.java index 71acad01..6e1790c9 100644 --- a/backend/src/main/java/com/group1/cuisines/entities/Dish.java +++ b/backend/src/main/java/com/group1/cuisines/entities/Dish.java @@ -34,6 +34,19 @@ public class Dish { @ManyToMany(mappedBy = "dishes") private List cuisines = new ArrayList<>(); + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Dish dish = (Dish) o; + return id != null && id.equals(dish.id); + } + + @Override + public int hashCode() { + return 31; + } + @Override public String toString() { return "Dish{" + diff --git a/backend/src/main/java/com/group1/cuisines/entities/Recipe.java b/backend/src/main/java/com/group1/cuisines/entities/Recipe.java index 10e33734..eadeb35c 100644 --- a/backend/src/main/java/com/group1/cuisines/entities/Recipe.java +++ b/backend/src/main/java/com/group1/cuisines/entities/Recipe.java @@ -4,6 +4,7 @@ import lombok.*; import java.util.ArrayList; +import java.util.Date; import java.util.List; @Data @@ -31,6 +32,11 @@ public class Recipe { @Builder.Default private List ratings = new ArrayList<>(); + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_at", nullable = false, updatable = false) + @org.hibernate.annotations.CreationTimestamp + private Date createdAt; + @ManyToOne @JoinColumn(name = "user_id", nullable = false) private User user; diff --git a/backend/src/main/java/com/group1/cuisines/entities/User.java b/backend/src/main/java/com/group1/cuisines/entities/User.java index 5e7d5923..16a2e4b7 100644 --- a/backend/src/main/java/com/group1/cuisines/entities/User.java +++ b/backend/src/main/java/com/group1/cuisines/entities/User.java @@ -1,10 +1,7 @@ package com.group1.cuisines.entities; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -18,6 +15,8 @@ @NoArgsConstructor @AllArgsConstructor @Entity +@Getter +@Setter @Table(name = "users") public class User implements UserDetails { @@ -53,6 +52,13 @@ public class User implements UserDetails { @Builder.Default private int recipeCount = 0; + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private Set recipes = new HashSet<>(); + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private Set bookmarks = new HashSet<>(); + + + diff --git a/backend/src/main/java/com/group1/cuisines/exceptions/GlobalExceptionHandler.java b/backend/src/main/java/com/group1/cuisines/exceptions/GlobalExceptionHandler.java new file mode 100644 index 00000000..bff8d299 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/exceptions/GlobalExceptionHandler.java @@ -0,0 +1,26 @@ +package com.group1.cuisines.exceptions; + +import com.group1.cuisines.dao.response.ApiResponse; +import org.springframework.boot.context.config.ConfigDataResourceNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception e) { + ApiResponse response = new ApiResponse<>(500, "An unexpected error occurred: " + e.getMessage(), null); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity> handleResourceNotFoundException(ResourceNotFoundException e) { + ApiResponse response = new ApiResponse<>(400, e.getMessage(), null); + return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + } + + +} \ No newline at end of file diff --git a/backend/src/main/java/com/group1/cuisines/exceptions/ResourceNotFoundException.java b/backend/src/main/java/com/group1/cuisines/exceptions/ResourceNotFoundException.java new file mode 100644 index 00000000..d1ab87bf --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/exceptions/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package com.group1.cuisines.exceptions; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/group1/cuisines/repositories/DishRepository.java b/backend/src/main/java/com/group1/cuisines/repositories/DishRepository.java index a1b94f43..7eedc5ac 100644 --- a/backend/src/main/java/com/group1/cuisines/repositories/DishRepository.java +++ b/backend/src/main/java/com/group1/cuisines/repositories/DishRepository.java @@ -1,24 +1,28 @@ package com.group1.cuisines.repositories; import com.group1.cuisines.entities.Dish; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.List; -import java.util.Optional; - @Repository -public interface DishRepository extends JpaRepository { - @Query("SELECT d FROM Dish d WHERE LOWER(d.name) LIKE LOWER(CONCAT('%', :name, '%'))") +public interface DishRepository extends JpaRepository { + @Query( + "SELECT d FROM Dish d WHERE LOWER(d.name) LIKE LOWER(CONCAT('%', :name, '%')) OR LOWER(d.countries) LIKE LOWER(CONCAT('%', :name, '%')) OR LOWER(d.ingredients) LIKE LOWER(CONCAT('%', :name, '%'))" + ) List findByNameContainingIgnoreCase(String name); - @Query("SELECT d FROM Dish d JOIN d.cuisines c WHERE LOWER(c.name) LIKE LOWER(CONCAT('%', :cuisineName, '%'))") + + @Query( + "SELECT d FROM Dish d JOIN d.cuisines c WHERE LOWER(c.name) LIKE LOWER(CONCAT('%', :cuisineName, '%'))" + ) List findByCuisinesName(@Param("cuisineName") String cuisineName); + List findByFoodTypesContainingIgnoreCase(String foodType); Optional findById(String id); @Query("SELECT d FROM Dish d JOIN d.cuisines c WHERE c.id = :cuisineId") List findByCuisineId(@Param("cuisineId") String cuisineId); - } diff --git a/backend/src/main/java/com/group1/cuisines/repositories/RecipeRepository.java b/backend/src/main/java/com/group1/cuisines/repositories/RecipeRepository.java index 9d027dd9..c34d8c78 100644 --- a/backend/src/main/java/com/group1/cuisines/repositories/RecipeRepository.java +++ b/backend/src/main/java/com/group1/cuisines/repositories/RecipeRepository.java @@ -3,10 +3,34 @@ import com.group1.cuisines.entities.Recipe; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository -public interface RecipeRepository extends JpaRepository { +public interface RecipeRepository extends JpaRepository { + + + + + @Query("SELECT r FROM Recipe r WHERE " + + "(:dishId IS NULL OR r.dish.id = :dishId) AND " + + "(:cuisineId IS NULL OR EXISTS (SELECT d FROM Dish d JOIN d.cuisines c WHERE d = r.dish AND c.id = :cuisineId)) " + + "ORDER BY CASE WHEN :sort = 'recent' THEN r.createdAt ELSE r.averageRating END DESC") + List findByDishIdAndCuisineIdWithSort( + @Param("dishId") String dishId, + @Param("cuisineId") String cuisineId, + @Param("sort") String sort); + + // Fetch recipes by dishId and cuisineId + @Query("SELECT r FROM Recipe r JOIN r.dish d JOIN d.cuisines c WHERE d.id = :dishId AND c.id = :cuisineId ORDER BY r.createdAt DESC") + List findByDishIdAndCuisineIdOrderByCreatedAtDesc(String dishId, String cuisineId); + + @Query("SELECT r FROM Recipe r JOIN r.dish d JOIN d.cuisines c WHERE d.id = :dishId AND c.id = :cuisineId ORDER BY r.averageRating DESC") + List findByDishIdAndCuisineIdOrderByAverageRatingDesc(String dishId, String cuisineId); + @Query("SELECT r FROM Recipe r JOIN r.dish d JOIN d.cuisines c WHERE d.id = :dishId AND c.id = :cuisineId") + List findByDishIdAndCuisineId(String dishId, String cuisineId); @Query("SELECT AVG(r.ratingValue) FROM Rating r WHERE r.recipe.id = :recipeId") Double findAverageByRecipeId(Integer recipeId); } diff --git a/backend/src/main/java/com/group1/cuisines/services/AuthenticationService.java b/backend/src/main/java/com/group1/cuisines/services/AuthenticationService.java index 06bb7823..1221f008 100644 --- a/backend/src/main/java/com/group1/cuisines/services/AuthenticationService.java +++ b/backend/src/main/java/com/group1/cuisines/services/AuthenticationService.java @@ -52,7 +52,7 @@ public ApiResponse signup( String token = jwtService.generateToken(user); return new ApiResponse<>( - 200, + 201, "User registered successfully.", AuthenticationTokenResponse.builder().token(token).build() ); diff --git a/backend/src/main/java/com/group1/cuisines/services/CuisineService.java b/backend/src/main/java/com/group1/cuisines/services/CuisineService.java index e65a84ca..e0211558 100644 --- a/backend/src/main/java/com/group1/cuisines/services/CuisineService.java +++ b/backend/src/main/java/com/group1/cuisines/services/CuisineService.java @@ -1,24 +1,43 @@ package com.group1.cuisines.services; +import com.group1.cuisines.dto.CuisineDetailsDto; +import com.group1.cuisines.dto.DishDto; import com.group1.cuisines.entities.Cuisine; +import com.group1.cuisines.entities.Dish; import com.group1.cuisines.repositories.CuisineRepository; +import jakarta.persistence.EntityNotFoundException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + @Service public class CuisineService { @Autowired private CuisineRepository cuisineRepository; - public Cuisine getCuisineById(String cuisineId, boolean includeDishes) { - return cuisineRepository.findById(cuisineId) - .map(cuisine -> { - if (!includeDishes) { - cuisine.setDishes(null); // Clear the dishes if not required - } - return cuisine; - }) - .orElse(null); + public CuisineDetailsDto getCuisineById(String cuisineId, boolean includeDishes) { + Cuisine cuisine = cuisineRepository.findById(cuisineId) + .orElseThrow(() -> new EntityNotFoundException("Cuisine not found")); + + CuisineDetailsDto detailsDto = new CuisineDetailsDto( + cuisine.getId(), + cuisine.getName(), + // cuisine.getDescription(), + + + includeDishes ? convertDishes(cuisine.getDishes()) : new ArrayList<>() + ); + return detailsDto; + } + + private List convertDishes(Set dishes) { + return dishes.stream() + .map(dish -> new DishDto(dish.getId(), dish.getName(),dish.getImage())) + .collect(Collectors.toList()); } } diff --git a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java index cc811ffe..51efe39f 100644 --- a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java +++ b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java @@ -1,19 +1,19 @@ package com.group1.cuisines.services; -import com.group1.cuisines.dto.CommentsDto; -import com.group1.cuisines.dto.IngredientsDto; -import com.group1.cuisines.dto.NewRecipeDto; -import com.group1.cuisines.dto.RecipeDetailDto; +import com.group1.cuisines.dto.*; import com.group1.cuisines.entities.*; import com.group1.cuisines.repositories.*; +import jakarta.annotation.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; @Service @@ -35,6 +35,25 @@ public class RecipeService { @Autowired private CommentRepository commentRepository; + + + + public List findRecipes(String sort, String dishId, String cuisineId) { + List recipes = recipeRepository.findByDishIdAndCuisineIdWithSort(dishId, cuisineId, sort); + + return recipes.stream() + .map(recipe -> new RecipeDto( + recipe.getId(), + recipe.getTitle(), + recipe.getInstructions(), + recipe.getPreparationTime(), + recipe.getCookingTime(), + recipe.getServingSize(), + recipe.getAverageRating(), + recipe.getTitle())) + .collect(Collectors.toList()); + } + @Transactional public RecipeDetailDto createRecipe(NewRecipeDto newRecipe, String username) throws Exception { Optional user = userRepository.findByUsername(username); @@ -150,6 +169,7 @@ public List getWhoBookmarked(Integer recipeId) { return bookmarkRepository.findByRecipeId(recipeId).stream().map(Bookmark::getUser).toList(); } + public List getCommentsByRecipeId(Integer recipeId) { return commentRepository.findByRecipeId(recipeId).stream() .map(comment -> CommentsDto.builder() @@ -162,4 +182,89 @@ public List getCommentsByRecipeId(Integer recipeId) { .build()) .collect(Collectors.toList()); } + + public RecipeDetailsDto getRecipeById(Integer recipeId) { + Optional recipe = recipeRepository.findById(recipeId); + + + if (recipe.isPresent()) { + CuisineDto cuisineDto = new CuisineDto(); + Recipe r = recipe.get(); + if (r.getDish() != null && !r.getDish().getCuisines().isEmpty()) { + + cuisineDto.setId(r.getDish().getCuisines().get(0).getId()); + cuisineDto.setName(r.getDish().getCuisines().get(0).getName()); + + } + else if(r.getDish() != null && r.getDish().getCuisines().isEmpty()){ + cuisineDto.setId("No cuisine Id from wikidata"); + cuisineDto.setName("No cuisine name from wikidata"); + } + // Conversion from Recipe entity to RecipeDetailsDto + return new RecipeDetailsDto( + r.getId(), + r.getTitle(), + + r.getInstructions(), + r.getIngredients().stream().map(ingredient -> new IngredientsDto( ingredient.getName())).collect(Collectors.toList()), + + r.getCookingTime(), + r.getServingSize(), + cuisineDto, + + new DishDto(r.getDish().getId(), r.getDish().getName(), r.getDish().getImage()), + r.getAverageRating(), + new AuthorDto(r.getUser().getId(), r.getUser().getFirstName() , r.getUser().getUsername(), r.getUser().getFollowing().size(),r.getUser().getFollowers().size(), r.getUser().getRecipeCount()) + + ); + } + return null; + } + + public List getRecipesByType(String type, @Nullable String username) { + if ("explore".equals(type)) { + return recipeRepository.findAll().stream() + .map(this::convertToRecipeDto) + .collect(Collectors.toList()); + } else if ("following".equals(type) && username != null) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found")); + return user.getFollowing().stream() + .flatMap(followingUser -> followingUser.getRecipes().stream()) + .map(this::convertToRecipeDto) + .collect(Collectors.toList()); + } + return new ArrayList<>(); // Return an empty list if username is null or other conditions are not met + } + + private RecipeDetailsDto convertToRecipeDto(Recipe r) { + CuisineDto cuisineDto = new CuisineDto(); + if (r.getDish() != null && !r.getDish().getCuisines().isEmpty()) { + + cuisineDto.setId(r.getDish().getCuisines().get(0).getId()); + cuisineDto.setName(r.getDish().getCuisines().get(0).getName()); + + } + else if(r.getDish() != null && r.getDish().getCuisines().isEmpty()){ + cuisineDto.setId("No cuisine Id from wikidata"); + cuisineDto.setName("No cuisine name from wikidata"); + } + // Conversion logic here + return new RecipeDetailsDto( + r.getId(), + r.getTitle(), + + r.getInstructions(), + r.getIngredients().stream().map(ingredient -> new IngredientsDto( ingredient.getName())).collect(Collectors.toList()), + + r.getCookingTime(), + r.getServingSize(), + cuisineDto, + + new DishDto(r.getDish().getId(), r.getDish().getName(), r.getDish().getImage()), + r.getAverageRating(), + new AuthorDto(r.getUser().getId(), r.getUser().getFirstName() , r.getUser().getUsername(),r.getUser().getFollowing().size(), r.getUser().getFollowers().size(), r.getUser().getRecipeCount()) + + ); + } } diff --git a/backend/src/main/java/com/group1/cuisines/services/SearchService.java b/backend/src/main/java/com/group1/cuisines/services/SearchService.java index ee33a83e..abbe6422 100644 --- a/backend/src/main/java/com/group1/cuisines/services/SearchService.java +++ b/backend/src/main/java/com/group1/cuisines/services/SearchService.java @@ -1,6 +1,9 @@ package com.group1.cuisines.services; +import com.group1.cuisines.dto.DishDto; +import com.group1.cuisines.dto.DishResponseDto; import com.group1.cuisines.entities.Dish; +import com.group1.cuisines.exceptions.ResourceNotFoundException; import com.group1.cuisines.repositories.DishRepository; import com.group1.cuisines.repositories.UserRepository; import lombok.RequiredArgsConstructor; @@ -16,17 +19,23 @@ public class SearchService { private final DishRepository dishRepository; - public List searchDishes(String query, String cuisine, String foodType) { + public List searchDishes(String query, String cuisine, String foodType) { List dishes = dishRepository.findAll(); // Filter by dish name if (query != null && !query.isEmpty()) { dishes = dishRepository.findByNameContainingIgnoreCase(query); + if (dishes.isEmpty()) { + throw new ResourceNotFoundException("No dishes found with the given name query."); + } } // Filter by cuisine name if (cuisine != null && !cuisine.isEmpty()) { List dishesByCuisine = dishRepository.findByCuisinesName(cuisine); + if (dishesByCuisine.isEmpty()) { + throw new ResourceNotFoundException("No dishes found with the given cuisine."); + } dishes = dishes.stream() .filter(dishesByCuisine::contains) .collect(Collectors.toList()); @@ -37,9 +46,25 @@ public List searchDishes(String query, String cuisine, String foodType) { dishes = dishes.stream() .filter(d -> d.getFoodTypes() != null && d.getFoodTypes().contains(foodType)) .collect(Collectors.toList()); + + if (dishes.isEmpty()) { + throw new ResourceNotFoundException("No dishes found with the given food type."); + } } - return dishes; + // Map to DishResponseDto + return dishes.stream() + .map(d -> new DishResponseDto( + d.getId(), + d.getName(), + d.getImage(), + d.getDescription(), + d.getCountries(), + d.getIngredients(), + d.getFoodTypes(), + d.getCuisines().isEmpty() ? null : d.getCuisines().get(0).getName() + )) + .collect(Collectors.toList()); } diff --git a/backend/src/main/java/com/group1/cuisines/services/UserService.java b/backend/src/main/java/com/group1/cuisines/services/UserService.java index 0332882e..7dacf859 100644 --- a/backend/src/main/java/com/group1/cuisines/services/UserService.java +++ b/backend/src/main/java/com/group1/cuisines/services/UserService.java @@ -1,7 +1,11 @@ package com.group1.cuisines.services; +import com.group1.cuisines.dto.*; +import com.group1.cuisines.entities.Bookmark; +import com.group1.cuisines.entities.Recipe; import com.group1.cuisines.entities.User; import com.group1.cuisines.repositories.UserRepository; +import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -14,6 +18,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -108,4 +113,94 @@ public Set getUserFollower(Integer userId) { } return Collections.emptySet(); } + + public UserProfileDto getUserProfileById(Integer userId, String currentUsername) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("User not found")); + + boolean isSelf = user.getUsername().equals(currentUsername); + + UserProfileDto profile = new UserProfileDto(); + profile.setId(user.getId()); + profile.setUsername(user.getUsername()); + profile.setName(user.getFirstName() + " " + user.getLastName()); + profile.setBio(user.getBio()); + profile.setFollowersCount(user.getFollowers().size()); + profile.setFollowingCount(user.getFollowing().size()); + profile.setRecipeCount(user.getRecipes().size()); + profile.setRecipes(user.getRecipes().stream() + .map(this::convertToRecipeDetailsDto) + .collect(Collectors.toList())); + + if (isSelf) { + profile.setBookmarks(user.getBookmarks().stream() + .map(this::convertToBookmarkDto) + .collect(Collectors.toList())); + } else { + profile.setBookmarks(Collections.emptyList()); + } + + return profile; + } + + private RecipeDetailsDto convertToRecipeDetailsDto(Recipe recipe) { + CuisineDto cuisineDto = new CuisineDto(); + if (recipe.getDish() != null && !recipe.getDish().getCuisines().isEmpty()) { + + cuisineDto.setId(recipe.getDish().getCuisines().get(0).getId()); + cuisineDto.setName(recipe.getDish().getCuisines().get(0).getName()); + + } + else if(recipe.getDish() != null && recipe.getDish().getCuisines().isEmpty()){ + cuisineDto.setId("No cuisine Id from wikidata"); + cuisineDto.setName("No cuisine name from wikidata"); + } + return new RecipeDetailsDto( + recipe.getId(), + recipe.getTitle(), + recipe.getInstructions(), + recipe.getIngredients().stream() + .map(ingredient -> new IngredientsDto(ingredient.getName())) + .collect(Collectors.toList()), + recipe.getCookingTime(), + recipe.getServingSize(), + cuisineDto, + new DishDto(recipe.getDish().getId(), recipe.getDish().getName(), recipe.getDish().getImage()), + recipe.getAverageRating(), + new AuthorDto(recipe.getUser().getId(), recipe.getUser().getUsername(), recipe.getUser().getFirstName(), + recipe.getUser().getFollowingCount(), recipe.getUser().getFollowerCount(),recipe.getUser().getRecipeCount()) + ); + } + + private BookmarkDto convertToBookmarkDto(Bookmark bookmark) { + + Recipe recipe = bookmark.getRecipe(); + CuisineDto cuisineDto = new CuisineDto(); + if (recipe.getDish() != null && !recipe.getDish().getCuisines().isEmpty()) { + + cuisineDto.setId(recipe.getDish().getCuisines().get(0).getId()); + cuisineDto.setName(recipe.getDish().getCuisines().get(0).getName()); + + } + else if(recipe.getDish() != null && recipe.getDish().getCuisines().isEmpty()){ + cuisineDto.setId("No cuisine Id from wikidata"); + cuisineDto.setName("No cuisine name from wikidata"); + } + return new BookmarkDto( + recipe.getId(), + recipe.getTitle(), + recipe.getInstructions(), + recipe.getIngredients().stream() + .map(ingredient -> new IngredientsDto(ingredient.getName())) + .collect(Collectors.toList()), + recipe.getServingSize(), + recipe.getCookingTime(), + //recipe.getImages(), + cuisineDto, + new DishDto(recipe.getDish().getId(), recipe.getDish().getName(), recipe.getDish().getImage()), + recipe.getAverageRating(), + new AuthorDto(recipe.getUser().getId(), recipe.getUser().getUsername(), recipe.getUser().getFirstName(), + recipe.getUser().getFollowingCount(), recipe.getUser().getFollowerCount(),recipe.getUser().getRecipeCount()) + ); + } } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 14a044e0..14898104 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -5,11 +5,15 @@ jwt.signing.key=413F4428472B4B6250655368566D5970337336763979244226452948404D6351 spring.application.name=cuisines spring.datasource.url=jdbc:mysql://localhost:3306/cuisines-test?createDatabaseIfNotExist=true spring.datasource.username=root -spring.datasource.password=adminadmin +spring.datasource.password=admin spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true logging.level.com.group1.cuisines=DEBUG +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.use_sql_comments=true +logging.level.org.hibernate.type.descriptor.sql=trace + spring.banner.location=./banner.txt diff --git a/backend/src/test/java/com/group1/cuisines/AuthenticationControllerTest.java b/backend/src/test/java/com/group1/cuisines/AuthenticationControllerTest.java index d3d6218b..4aa77e4c 100644 --- a/backend/src/test/java/com/group1/cuisines/AuthenticationControllerTest.java +++ b/backend/src/test/java/com/group1/cuisines/AuthenticationControllerTest.java @@ -14,6 +14,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; public class AuthenticationControllerTest { @@ -31,123 +32,65 @@ public void setUp() { @Test public void shouldReturnSuccessOnValidSignin() { - SignInRequest signInRequest = new SignInRequest( - "testUser", - "testPassword" - ); - ApiResponse apiResponse = - new ApiResponse<>( - 200, - "Success", - new AuthenticationTokenResponse("token") - ); - when(authenticationService.signin(signInRequest)).thenReturn( - apiResponse - ); - ResponseEntity< - ApiResponse - > responseEntity = authenticationController.signin(signInRequest); - assertEquals( - 200, - responseEntity.getBody().getStatus(), - "Status code does not match expected value on successful signin" - ); - assertEquals( - apiResponse, - responseEntity.getBody(), - "Response body does not match expected value on successful signin" - ); + SignInRequest signInRequest = new SignInRequest("testUser", "testPassword"); + ApiResponse apiResponse = new ApiResponse<>(200, "Success", new AuthenticationTokenResponse("token")); + when(authenticationService.signin(signInRequest)).thenReturn(apiResponse); + + ResponseEntity> responseEntity = authenticationController.signin(signInRequest); + + assertEquals(HttpStatus.OK.value(), responseEntity.getStatusCodeValue(), "Status code does not match expected value on successful signin"); + assertEquals(apiResponse, responseEntity.getBody(), "Response body does not match expected value on successful signin"); } @Test public void shouldReturnFailureOnInvalidSignin() { - SignInRequest signInRequest = new SignInRequest( - "testUser", - "wrongPassword" - ); - ApiResponse apiResponse = - new ApiResponse<>(401, "Failure", null); - when(authenticationService.signin(signInRequest)).thenReturn( - apiResponse - ); - ResponseEntity< - ApiResponse - > responseEntity = authenticationController.signin(signInRequest); - assertEquals( - 401, - responseEntity.getBody().getStatus(), - "Status code does not match expected value on failed signin" - ); - assertEquals( - apiResponse, - responseEntity.getBody(), - "Response body does not match expected value on failed signin" - ); + SignInRequest signInRequest = new SignInRequest("testUser", "wrongPassword"); + ApiResponse apiResponse = new ApiResponse<>(401, "Invalid email/username or password.", null); + when(authenticationService.signin(signInRequest)).thenReturn(apiResponse); + + ResponseEntity> responseEntity = authenticationController.signin(signInRequest); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), responseEntity.getStatusCodeValue(), "Status code does not match expected value on failed signin"); + assertEquals(apiResponse, responseEntity.getBody(), "Response body does not match expected value on failed signin"); } @Test public void shouldReturnSuccessOnValidSignup() { SignUpRequest signUpRequest = SignUpRequest.builder() - .email("newUser@gmail.com") - .username("newUser") - .country("USA") - .bio("Bio of the new user") - .password("newPassword") - .firstName("New") - .lastName("User") - .build(); - ApiResponse apiResponse = - new ApiResponse<>( - 200, - "Success", - new AuthenticationTokenResponse("token") - ); - when(authenticationService.signup(signUpRequest)).thenReturn( - apiResponse - ); - ResponseEntity< - ApiResponse - > responseEntity = authenticationController.signup(signUpRequest); - assertEquals( - 200, - responseEntity.getBody().getStatus(), - "Status code does not match expected value on successful signup" - ); - assertEquals( - apiResponse, - responseEntity.getBody(), - "Response body does not match expected value on successful signup" - ); + .email("newUser@gmail.com") + .username("newUser") + .country("USA") + .bio("Bio of the new user") + .password("newPassword") + .firstName("New") + .lastName("User") + .build(); + ApiResponse apiResponse = new ApiResponse<>(201, "User registered successfully.", new AuthenticationTokenResponse("token")); + when(authenticationService.signup(signUpRequest)).thenReturn(apiResponse); + + ResponseEntity> responseEntity = authenticationController.signup(signUpRequest); + + assertEquals(HttpStatus.CREATED.value(), responseEntity.getStatusCodeValue(), "Status code does not match expected value on successful signup"); + assertEquals(apiResponse, responseEntity.getBody(), "Response body does not match expected value on successful signup"); } @Test public void shouldReturnFailureOnInvalidSignup() { SignUpRequest signUpRequest = SignUpRequest.builder() - .email("newUser@gmail.com") - .username("newUser") - .country("USA") - .bio("Bio of the new user") - .password("newPassword") - .firstName("New") - .lastName("User") - .build(); - ApiResponse apiResponse = - new ApiResponse<>(409, "Failure", null); - when(authenticationService.signup(signUpRequest)).thenReturn( - apiResponse - ); - ResponseEntity< - ApiResponse - > responseEntity = authenticationController.signup(signUpRequest); - assertEquals( - 409, - responseEntity.getBody().getStatus(), - "Status code does not match expected value on failed signup" - ); - assertEquals( - apiResponse, - responseEntity.getBody(), - "Response body does not match expected value on failed signup" - ); + .email("newUser@gmail.com") + .username("newUser") + .country("USA") + .bio("Bio of the new user") + .password("newPassword") + .firstName("New") + .lastName("User") + .build(); + ApiResponse apiResponse = new ApiResponse<>(409, "Email or username already exists.", null); + when(authenticationService.signup(signUpRequest)).thenReturn(apiResponse); + + ResponseEntity> responseEntity = authenticationController.signup(signUpRequest); + + assertEquals(HttpStatus.CONFLICT.value(), responseEntity.getStatusCodeValue(), "Status code does not match expected value on failed signup"); + assertEquals(apiResponse, responseEntity.getBody(), "Response body does not match expected value on failed signup"); } }