Merge remote-tracking branch 'refs/remotes/origin/main'

This commit is contained in:
kaipher7
2026-04-16 17:38:07 -06:00
13 changed files with 416 additions and 155 deletions
@@ -10,24 +10,29 @@ import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
@Configuration @Configuration
public class SecurityConfig { public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public PasswordEncoder passwordEncoder() {
http .csrf(csrf->csrf.disable()) return new BCryptPasswordEncoder();
.authorizeHttpRequests(auth -> auth }
.requestMatchers("/login", "/register", "/css/**", "/images/**").permitAll()
.anyRequest().authenticated() @Bean
) public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
.formLogin(form -> form http
.loginPage("/login") .csrf(csrf -> csrf.disable())
.defaultSuccessUrl("/", true) .authorizeHttpRequests(auth -> auth
.permitAll() .requestMatchers("/login", "/register", "/css/**", "/images/**").permitAll()
) .requestMatchers("/api/users").permitAll()
.logout(logout -> logout.permitAll()); .requestMatchers("/api/admin/**").hasRole("ADMIN")
return http.build(); .anyRequest().authenticated()
} )
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/", true)
.permitAll()
)
.logout(logout -> logout.permitAll());
return http.build();
}
} }
@@ -0,0 +1,39 @@
package com.example.demo.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.example.demo.dto.UserDto;
import com.example.demo.service.UserService;
@RestController
@RequestMapping("/api/admin")
public class AdminController {
private final UserService userService;
public AdminController(UserService userService) {
this.userService = userService;
}
@PostMapping("/users/{id}/ban")
public ResponseEntity<UserDto> banUser(@PathVariable Integer id) {
return new ResponseEntity<>(userService.banUser(id), HttpStatus.OK);
}
@PostMapping("/users/{id}/unban")
public ResponseEntity<UserDto> unbanUser(@PathVariable Integer id) {
return new ResponseEntity<>(userService.unbanUser(id), HttpStatus.OK);
}
@PostMapping("/users/{id}/make-admin")
public ResponseEntity<UserDto> makeAdmin(@PathVariable Integer id) {
return new ResponseEntity<>(userService.makeAdmin(id), HttpStatus.OK);
}
@PostMapping("/users/{id}/make-user")
public ResponseEntity<UserDto> makeUser(@PathVariable Integer id) {
return new ResponseEntity<>(userService.makeUser(id), HttpStatus.OK);
}
}
@@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.security.core.Authentication;
import com.example.demo.dto.RecipeDto; import com.example.demo.dto.RecipeDto;
import com.example.demo.dto.UserDto; import com.example.demo.dto.UserDto;
@@ -35,9 +36,9 @@ public class RecipeController {
// build create recipe REST API // build create recipe REST API
@PostMapping @PostMapping
public ResponseEntity<RecipeDto> saveRecipe(@Valid @RequestBody Recipe recipe) { public ResponseEntity<RecipeDto> saveRecipe(@RequestBody RecipeDto recipeDto, Authentication authentication) {
RecipeDto recipeDto = recipeService.convertToDto(recipe); String currentUsername = authentication.getName();
return new ResponseEntity<RecipeDto>(recipeService.saveRecipe(recipeDto), HttpStatus.CREATED); return new ResponseEntity<>(recipeService.saveRecipe(recipeDto, currentUsername), HttpStatus.CREATED);
} }
// build get all recipes REST API // build get all recipes REST API
@@ -69,17 +70,21 @@ public class RecipeController {
// build update recipe REST API // build update recipe REST API
// http://localhost:8080/api/recipes/(id number goes here) // http://localhost:8080/api/recipes/(id number goes here)
@PutMapping("{id}") @PutMapping("{id}")
public ResponseEntity<RecipeDto> updateRecipe(@PathVariable("id") Integer recipeId, @RequestBody Recipe recipe) { public ResponseEntity<RecipeDto> updateRecipe(
RecipeDto recipeDto = recipeService.convertToDto(recipe); @PathVariable("id") Integer recipeId,
return new ResponseEntity<RecipeDto>(recipeService.updateRecipe(recipeDto, recipeId), HttpStatus.OK); @RequestBody RecipeDto recipeDto,
Authentication authentication) {
String currentUsername = authentication.getName();
return new ResponseEntity<>(recipeService.updateRecipe(recipeDto, recipeId, currentUsername), HttpStatus.OK);
} }
// build delete recipe REST API // build delete recipe REST API
// http://localhost:8080/api/recipes/(id number goes here) // http://localhost:8080/api/recipes/(id number goes here)
@DeleteMapping("{id}") @DeleteMapping("{id}")
public ResponseEntity<String> deleteRecipe(@PathVariable("id") Integer recipeId) { public ResponseEntity<String> deleteRecipe(@PathVariable("id") Integer recipeId, Authentication authentication) {
recipeService.deleteRecipe(recipeId); String currentUsername = authentication.getName();
return new ResponseEntity<String>("Recipe deleted succesfully!", HttpStatus.OK); recipeService.deleteRecipe(recipeId, currentUsername);
return new ResponseEntity<>("Recipe deleted successfully!", HttpStatus.OK);
} }
} }
@@ -1,8 +1,6 @@
package com.example.demo.controller; package com.example.demo.controller;
import java.security.Principal;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -15,26 +13,20 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import com.example.demo.dto.UserDto; import com.example.demo.dto.UserDto;
import com.example.demo.entity.User; import com.example.demo.entity.User;
import com.example.demo.service.UserService; import com.example.demo.service.UserService;
import com.example.demo.repository.UserRepo;
@RestController @RestController
@RequestMapping("/api/users") @RequestMapping("/api/users")
public class UserController { public class UserController {
private UserService userService; private UserService userService;
private UserRepo userRepo;
public UserController(UserService userService, UserRepo userRepo) { public UserController(UserService userService) {
super(); super();
this.userService = userService; this.userService = userService;
this.userRepo = userRepo;
} }
// build create user REST API // build create user REST API
@@ -52,21 +44,11 @@ public class UserController {
// build get user by name REST API // build get user by name REST API
@GetMapping("/search") @GetMapping("/search")
public ResponseEntity<List<UserDto>> getUsersByName(@RequestParam String string) { public ResponseEntity<List<UserDto>> getUsersByName(@RequestParam String name) {
List<UserDto> users = userService.getUsersByName(string); List<UserDto> users = userService.getUsersByName(name);
return new ResponseEntity<>(users, HttpStatus.OK); return new ResponseEntity<>(users, HttpStatus.OK);
} }
// build get current user REST API
@GetMapping("/me")
public UserDto getLoggedInUser(Principal principal) {
if (principal == null) return null;
String username = principal.getName();
User user = (userRepo.findByUsername(username))
.orElse(null);
return userService.convertToDto(user);
}
// build get user by id REST API // build get user by id REST API
// http://localhost:8080/api/users/(id number goes here) // http://localhost:8080/api/users/(id number goes here)
@@ -5,7 +5,6 @@ import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive; import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -21,20 +20,17 @@ public class Recipe {
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id; private Integer id;
@Column(columnDefinition = "TEXT")
@NotBlank(message = "Please provide a recipe title") @NotBlank(message = "Please provide a recipe title")
@Size(max = 128, message = "Title cannot be longer than 128 characters")
private String title; private String title;
@Column(columnDefinition = "TEXT") @Column(columnDefinition = "TEXT")
@Size(max = 500, message = "Description cannot be longer than 500 characters")
private String description; private String description;
@NotNull(message = "Please Provide a prep time amount in minutes") @NotNull(message = "Please Provide a prep time amount in mintutes")
@Positive(message = "This value cannot be negative") @Positive(message = "This value cannot be negative")
private Integer prepTimeMinutes; private Integer prepTimeMinutes;
@NotNull(message = "Please Provide a cook time amount in minutes") @NotNull(message = "Please Provide a cook time amount in mintutes")
@Positive(message = "This value cannot be negative") @Positive(message = "This value cannot be negative")
private Integer cookTimeMinutes; private Integer cookTimeMinutes;
@@ -56,13 +52,10 @@ public class Recipe {
// Recipe ingredients relationship // Recipe ingredients relationship
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@NotEmpty(message = "At least one ingredient is required") @NotEmpty(message = "At least one ingredient is required")
@Size(max = 256, message = "Cannot have more than 256 ingredients")
private Set<RecipeIngredient> recipeIngredients = new HashSet<>(); private Set<RecipeIngredient> recipeIngredients = new HashSet<>();
// Recipe Steps relationship // Recipe Steps relationship
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@NotEmpty(message = "At least one step is required")
@Size(max = 50, message = "Cannot have more than 50 steps")
private Set<Step> steps = new HashSet<>(); private Set<Step> steps = new HashSet<>();
// Recipe Images relationship // Recipe Images relationship
@@ -1,28 +1,26 @@
package com.example.demo.entity; package com.example.demo.entity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType; import jakarta.persistence.FetchType;
import jakarta.persistence.Table; import jakarta.persistence.GeneratedValue;
import jakarta.validation.constraints.NotBlank; import jakarta.persistence.GenerationType;
import jakarta.validation.constraints.NotNull;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable; import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany; import jakarta.persistence.ManyToMany;
import jakarta.persistence.OneToMany; import jakarta.persistence.OneToMany;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.Table;
import jakarta.persistence.GenerationType;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
@Entity @Entity
@@ -31,17 +29,15 @@ public class User implements UserDetails {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@NotNull
private Integer id; private Integer id;
@Column(nullable = false, unique = true) @Column(nullable = false, unique = true)
@NotBlank(message = "Username is required")
private String username; private String username;
@Column(nullable = false)
private String role; private String role;
@Column(unique = true) @Column(unique = true)
@NotBlank(message = "Email is required")
private String email; private String email;
private String hashedpassword; private String hashedpassword;
@@ -49,20 +45,23 @@ public class User implements UserDetails {
@Column(name = "created_at") @Column(name = "created_at")
private LocalDateTime createdAt; private LocalDateTime createdAt;
// User Recipe relationship @Column(nullable = false)
private boolean banned = false;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private Set<Recipe> recipes = new HashSet<>(); private Set<Recipe> recipes = new HashSet<>();
// Favorite relationship and also junction table
@ManyToMany(fetch = FetchType.LAZY) @ManyToMany(fetch = FetchType.LAZY)
@JsonIgnore @JoinTable(
@JoinTable(name = "favorites", joinColumns = { @JoinColumn(name = "userId") }, inverseJoinColumns = { name = "favorites",
@JoinColumn(name = "recipeId") }) joinColumns = { @JoinColumn(name = "userId") },
inverseJoinColumns = { @JoinColumn(name = "recipeId") }
)
private Set<Recipe> FavRecipes = new HashSet<>(); private Set<Recipe> FavRecipes = new HashSet<>();
@Override @Override
public Collection<? extends GrantedAuthority> getAuthorities() { public Collection<? extends GrantedAuthority> getAuthorities() {
return new ArrayList<>(); return List.of(new SimpleGrantedAuthority(role));
} }
@Override @Override
@@ -75,6 +74,26 @@ public class User implements UserDetails {
return username; return username;
} }
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !banned;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return !banned;
}
public User() { public User() {
} }
@@ -84,6 +103,7 @@ public class User implements UserDetails {
this.email = email; this.email = email;
this.hashedpassword = hashedpassword; this.hashedpassword = hashedpassword;
this.createdAt = createdAt; this.createdAt = createdAt;
this.banned = false;
} }
public Integer getId() { public Integer getId() {
@@ -130,12 +150,27 @@ public class User implements UserDetails {
this.createdAt = createdAt; this.createdAt = createdAt;
} }
public boolean isBanned() {
return banned;
}
public void setBanned(boolean banned) {
this.banned = banned;
}
public Set<Recipe> getRecipes() {
return recipes;
}
public void setRecipes(Set<Recipe> recipes) {
this.recipes = recipes;
}
public Set<Recipe> getFavRecipes() { public Set<Recipe> getFavRecipes() {
return FavRecipes; return FavRecipes;
} }
public void setFavRecipes(Set<Recipe> favRecipes) { public void setFavRecipes(Set<Recipe> favRecipes) {
FavRecipes = favRecipes; this.FavRecipes = favRecipes;
} }
} }
@@ -1,5 +1,6 @@
package com.example.demo.repository; package com.example.demo.repository;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
@@ -10,4 +11,8 @@ public interface RecipeRepo extends JpaRepository<Recipe, Integer> {
List<Recipe> findByTitleContainingIgnoreCase(String name); List<Recipe> findByTitleContainingIgnoreCase(String name);
List<Recipe> findByTitleContainingIgnoreCaseAndTags_NameIn(String title, List<String> tags); List<Recipe> findByTitleContainingIgnoreCaseAndTags_NameIn(String title, List<String> tags);
long countByUserIdAndCreatedAtAfter(Integer userId, LocalDateTime after);
List<Recipe> findByUserId(Integer userId);
} }
@@ -17,7 +17,8 @@ public class CustomUserDetailsService implements UserDetailsService {
} }
@Override @Override
public UserDetails loadUserByUsername(@NonNull String username) throws UsernameNotFoundException { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepo.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("User not found")); return userRepo.findByUsername(username)
} .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
}
} }
@@ -1,9 +1,11 @@
package com.example.demo.service.Impl; package com.example.demo.service.Impl;
import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.example.demo.dto.RecipeDto; import com.example.demo.dto.RecipeDto;
@@ -55,6 +57,7 @@ public class RecipeServiceImpl implements RecipeService {
this.tagRepository = tagRepository; this.tagRepository = tagRepository;
} }
@Override
public RecipeDto convertToDto(Recipe recipe) { public RecipeDto convertToDto(Recipe recipe) {
List<RecipeIngredientDto> ingredientDtos = recipe.getRecipeIngredients().stream() List<RecipeIngredientDto> ingredientDtos = recipe.getRecipeIngredients().stream()
.map(ri -> new RecipeIngredientDto(ri.getIngredient().getName(), ri.getQuantity(), ri.getUnit(), .map(ri -> new RecipeIngredientDto(ri.getIngredient().getName(), ri.getQuantity(), ri.getUnit(),
@@ -71,34 +74,79 @@ public class RecipeServiceImpl implements RecipeService {
UserDto userDto = new UserDto(recipe.getUser().getId(), recipe.getUser().getUsername(), UserDto userDto = new UserDto(recipe.getUser().getId(), recipe.getUser().getUsername(),
recipe.getUser().getEmail()); recipe.getUser().getEmail());
return new RecipeDto(recipe.getTitle(), recipe.getDescription(), recipe.getPrepTimeMinutes(), RecipeDto dto = new RecipeDto(recipe.getTitle(), recipe.getDescription(), recipe.getPrepTimeMinutes(),
recipe.getCookTimeMinutes(), recipe.getServings(), userDto, recipe.getStatus(), ingredientDtos, recipe.getCookTimeMinutes(), recipe.getServings(), userDto, recipe.getStatus(), ingredientDtos,
stepDtos, imageDtos, tagDtos); stepDtos, imageDtos, tagDtos);
dto.setId(recipe.getId());
return dto;
}
private User getCurrentUser(String currentUsername) {
return userRepository.findByUsername(currentUsername)
.orElseThrow(() -> new NotFoundException("User", "username", currentUsername));
}
private boolean isAdmin(User user) {
return "ROLE_ADMIN".equals(user.getRole());
}
private void ensureUserNotBanned(User user) {
if (user.isBanned()) {
throw new AccessDeniedException("Banned users cannot perform this action.");
}
}
private void enforceUploadLimit(User user) {
if (isAdmin(user)) {
return;
}
LocalDateTime cutoff = LocalDateTime.now().minusHours(24);
long uploadsInLast24Hours = recipeRepository.countByUserIdAndCreatedAtAfter(user.getId(), cutoff);
if (uploadsInLast24Hours >= 10) {
throw new AccessDeniedException("Upload limit reached. Maximum is 10 recipes per 24 hours.");
}
}
private void enforceOwnerOrAdmin(User currentUser, Recipe recipe) {
if (isAdmin(currentUser)) {
return;
}
if (!recipe.getUser().getId().equals(currentUser.getId())) {
throw new AccessDeniedException("You do not have permission to modify this recipe.");
}
} }
@Override @Override
@Transactional @Transactional
public RecipeDto saveRecipe(RecipeDto dto) { public RecipeDto saveRecipe(RecipeDto dto, String currentUsername) {
User user = userRepository.findById(dto.getUserDto().getId()) User currentUser = getCurrentUser(currentUsername);
.orElseThrow(() -> new NotFoundException("User", "id", dto.getUserDto().getId())); ensureUserNotBanned(currentUser);
enforceUploadLimit(currentUser);
Recipe recipe = new Recipe(dto.getTitle(), dto.getDescription(), dto.getPrepTimeMinutes(), Recipe recipe = new Recipe(dto.getTitle(), dto.getDescription(), dto.getPrepTimeMinutes(),
dto.getCookTimeMinutes(), dto.getServings(), user, dto.getStatus()); dto.getCookTimeMinutes(), dto.getServings(), currentUser, dto.getStatus());
for (RecipeIngredientDto riDto : dto.getIngredients()) { if (dto.getIngredients() != null) {
for (RecipeIngredientDto riDto : dto.getIngredients()) {
Ingredient ingredient = ingredientRepository.findByNameIgnoreCase(riDto.getIngredientName()) Ingredient ingredient = ingredientRepository.findByNameIgnoreCase(riDto.getIngredientName())
.orElseGet(() -> new Ingredient(riDto.getIngredientName())); .orElseGet(() -> new Ingredient(riDto.getIngredientName()));
if (ingredient.getId() == null) { if (ingredient.getId() == null) {
ingredientRepository.save(ingredient); ingredientRepository.save(ingredient);
}
RecipeIngredient ri = new RecipeIngredient(recipe, ingredient, riDto.getQuantity(), riDto.getUnit(),
riDto.getNotes());
recipe.getRecipeIngredients().add(ri);
} }
RecipeIngredient ri = new RecipeIngredient(recipe, ingredient, riDto.getQuantity(), riDto.getUnit(),
riDto.getNotes());
recipe.getRecipeIngredients().add(ri);
} }
if (dto.getSteps() != null) { if (dto.getSteps() != null) {
@@ -115,20 +163,21 @@ public class RecipeServiceImpl implements RecipeService {
} }
} }
for (TagDto tDto : dto.getTags()) { if (dto.getTags() != null) {
for (TagDto tDto : dto.getTags()) {
Tag tag = tagRepository.findByName(tDto.getName()).orElseGet(() -> new Tag(tDto.getName())); Tag tag = tagRepository.findByName(tDto.getName()).orElseGet(() -> new Tag(tDto.getName()));
if (tag.getId() == null) { if (tag.getId() == null) {
tagRepository.save(tag); tagRepository.save(tag);
}
recipe.getTags().add(tag);
} }
recipe.getTags().add(tag);
} }
Recipe saved = recipeRepository.save(recipe); Recipe saved = recipeRepository.save(recipe);
return getRecipeById(saved.getId()); return getRecipeById(saved.getId());
//return convertToDto(saved);
} }
@Override @Override
@@ -152,10 +201,15 @@ public class RecipeServiceImpl implements RecipeService {
@Override @Override
@Transactional @Transactional
public RecipeDto updateRecipe(RecipeDto recipeDto, Integer id) { public RecipeDto updateRecipe(RecipeDto recipeDto, Integer id, String currentUsername) {
User currentUser = getCurrentUser(currentUsername);
ensureUserNotBanned(currentUser);
Recipe existingRecipe = recipeRepository.findById(id) Recipe existingRecipe = recipeRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Recipe", "id", id)); .orElseThrow(() -> new NotFoundException("Recipe", "id", id));
enforceOwnerOrAdmin(currentUser, existingRecipe);
existingRecipe.setTitle(recipeDto.getTitle()); existingRecipe.setTitle(recipeDto.getTitle());
existingRecipe.setDescription(recipeDto.getDescription()); existingRecipe.setDescription(recipeDto.getDescription());
existingRecipe.setPrepTimeMinutes(recipeDto.getPrepTimeMinutes()); existingRecipe.setPrepTimeMinutes(recipeDto.getPrepTimeMinutes());
@@ -197,13 +251,10 @@ public class RecipeServiceImpl implements RecipeService {
for (RecipeIngredientDto riDto : updatedIngredients) { for (RecipeIngredientDto riDto : updatedIngredients) {
// go through the old list of ingredients until we find a match with updated
// list
RecipeIngredient existingRI = existingRecipe.getRecipeIngredients().stream() RecipeIngredient existingRI = existingRecipe.getRecipeIngredients().stream()
.filter(ri -> ri.getIngredient().getName().equals(riDto.getIngredientName())).findFirst() .filter(ri -> ri.getIngredient().getName().equals(riDto.getIngredientName())).findFirst()
.orElse(null); .orElse(null);
// if old ingredient just update parameters
if (existingRI != null) { if (existingRI != null) {
existingRI.setQuantity(riDto.getQuantity()); existingRI.setQuantity(riDto.getQuantity());
@@ -211,7 +262,6 @@ public class RecipeServiceImpl implements RecipeService {
existingRI.setNotes(riDto.getNotes()); existingRI.setNotes(riDto.getNotes());
} }
// if new ingredient, have to make a whole new thing
else { else {
Ingredient ingredient = ingredientRepository.findByNameIgnoreCase(riDto.getIngredientName()) Ingredient ingredient = ingredientRepository.findByNameIgnoreCase(riDto.getIngredientName())
@@ -229,7 +279,6 @@ public class RecipeServiceImpl implements RecipeService {
} }
if (updatedSteps != null) { if (updatedSteps != null) {
// find steps that weren't included
for (Step step : existingRecipe.getSteps()) { for (Step step : existingRecipe.getSteps()) {
boolean existsInUpdatedList = updatedSteps.stream() boolean existsInUpdatedList = updatedSteps.stream()
.anyMatch(dto -> dto.getStepNumber().equals(step.getStepNumber())); .anyMatch(dto -> dto.getStepNumber().equals(step.getStepNumber()));
@@ -237,22 +286,17 @@ public class RecipeServiceImpl implements RecipeService {
if (!existsInUpdatedList) if (!existsInUpdatedList)
stepsToRemove.add(step); stepsToRemove.add(step);
} }
// delete those steps
existingRecipe.getSteps().removeAll(stepsToRemove); existingRecipe.getSteps().removeAll(stepsToRemove);
// go through updated steps
for (StepDto stepDto : updatedSteps) { for (StepDto stepDto : updatedSteps) {
// find matching step by step number
Step existingStep = existingRecipe.getSteps().stream() Step existingStep = existingRecipe.getSteps().stream()
.filter(s -> s.getStepNumber().equals(stepDto.getStepNumber())).findFirst().orElse(null); .filter(s -> s.getStepNumber().equals(stepDto.getStepNumber())).findFirst().orElse(null);
// if there's a match update the instruction string
if (existingStep != null) { if (existingStep != null) {
existingStep.setInstruction(stepDto.getInstruction()); existingStep.setInstruction(stepDto.getInstruction());
} }
// if no match then make a whole new step
else { else {
Step newStep = new Step(existingRecipe, stepDto.getStepNumber(), stepDto.getInstruction()); Step newStep = new Step(existingRecipe, stepDto.getStepNumber(), stepDto.getInstruction());
existingRecipe.getSteps().add(newStep); existingRecipe.getSteps().add(newStep);
@@ -260,7 +304,6 @@ public class RecipeServiceImpl implements RecipeService {
} }
} }
// same process as above just with images instead
if (updatedImages != null) { if (updatedImages != null) {
for (Image image : existingRecipe.getImages()) { for (Image image : existingRecipe.getImages()) {
boolean existsInUpdatedList = updatedImages.stream() boolean existsInUpdatedList = updatedImages.stream()
@@ -284,8 +327,6 @@ public class RecipeServiceImpl implements RecipeService {
} }
} }
// same process as above just with tags instead, except for saving the tag
// since the relationship for this one was slightly different
if (updatedTags != null) { if (updatedTags != null) {
for (Tag tag : existingRecipe.getTags()) { for (Tag tag : existingRecipe.getTags()) {
boolean existsInUpdatedList = updatedTags.stream().anyMatch(dto -> dto.getName().equals(tag.getName())); boolean existsInUpdatedList = updatedTags.stream().anyMatch(dto -> dto.getName().equals(tag.getName()));
@@ -314,39 +355,45 @@ public class RecipeServiceImpl implements RecipeService {
} }
@Override @Override
public void deleteRecipe(Integer Id) { @Transactional
recipeRepository.findById(Id).orElseThrow(() -> new NotFoundException("Recipe", "id", Id)); public void deleteRecipe(Integer id, String currentUsername) {
recipeRepository.deleteById(Id); User currentUser = getCurrentUser(currentUsername);
ensureUserNotBanned(currentUser);
Recipe recipe = recipeRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Recipe", "id", id));
enforceOwnerOrAdmin(currentUser, recipe);
recipeRepository.delete(recipe);
} }
@Override @Override
@Transactional @Transactional
public List<RecipeDto> getRecipes(String name, List<String> tags) { public List<RecipeDto> getRecipes(String name, List<String> tags) {
List<Recipe> recipes; List<Recipe> recipes;
if(!name.isBlank()) { if (!name.isBlank()) {
recipes = recipeRepository.findByTitleContainingIgnoreCase(name); recipes = recipeRepository.findByTitleContainingIgnoreCase(name);
} }
else { else {
recipes = recipeRepository.findAll(); recipes = recipeRepository.findAll();
} }
if(!tags.isEmpty() && !recipes.isEmpty()) { if (!tags.isEmpty() && !recipes.isEmpty()) {
recipes = recipes.stream() recipes = recipes.stream()
.filter(recipe -> recipe.getTags().stream().anyMatch(tag -> tags.contains(tag.getName()))) .filter(recipe -> recipe.getTags().stream().anyMatch(tag -> tags.contains(tag.getName())))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
List<RecipeDto> recipeList = new ArrayList<>(); List<RecipeDto> recipeList = new ArrayList<>();
for (Recipe recipe : recipes) { for (Recipe recipe : recipes) {
RecipeDto dto = convertToDto(recipe); RecipeDto dto = convertToDto(recipe);
recipeList.add(dto); recipeList.add(dto);
} }
return recipeList; return recipeList;
} }
} }
@@ -1,4 +1,4 @@
package com.example.demo.service.Impl; ppackage com.example.demo.service.Impl;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -39,6 +39,10 @@ public class UserServiceImpl implements UserService {
@Override @Override
public User saveUser(User user) { public User saveUser(User user) {
if (user.getRole() == null || user.getRole().isBlank()) {
user.setRole("ROLE_USER");
}
user.setBanned(false);
return userRepository.save(user); return userRepository.save(user);
} }
@@ -125,4 +129,39 @@ public class UserServiceImpl implements UserService {
return userList; return userList;
} }
@Override
public UserDto banUser(Integer id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new NotFoundException("User", "id", id));
user.setBanned(true);
userRepository.save(user);
return convertToDto(user);
}
@Override
public UserDto unbanUser(Integer id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new NotFoundException("User", "id", id));
user.setBanned(false);
userRepository.save(user);
return convertToDto(user);
}
@Override
public UserDto makeAdmin(Integer id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new NotFoundException("User", "id", id));
user.setRole("ROLE_ADMIN");
userRepository.save(user);
return convertToDto(user);
}
@Override
public UserDto makeUser(Integer id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new NotFoundException("User", "id", id));
user.setRole("ROLE_USER");
userRepository.save(user);
return convertToDto(user);
}
} }
@@ -11,7 +11,7 @@ import com.example.demo.entity.User;
public interface RecipeService { public interface RecipeService {
RecipeDto convertToDto(Recipe recipe); RecipeDto convertToDto(Recipe recipe);
RecipeDto saveRecipe(RecipeDto recipe); RecipeDto saveRecipe(RecipeDto recipe, String currentUsername);
List<RecipeDto> getAllRecipes(); List<RecipeDto> getAllRecipes();
@@ -19,8 +19,9 @@ public interface RecipeService {
List<RecipeDto> getRecipes(String name, List<String> tags); List<RecipeDto> getRecipes(String name, List<String> tags);
RecipeDto updateRecipe(RecipeDto recipedto, Integer Id); RecipeDto updateRecipe(RecipeDto recipedto, Integer Id, String currentUsername);
void deleteRecipe(Integer Id); void deleteRecipe(Integer Id, String currentUsername);
} }
@@ -12,15 +12,23 @@ public interface UserService {
List<UserDto> getAllUsers(); List<UserDto> getAllUsers();
UserDto getUserById(Integer Id); UserDto getUserById(Integer id);
List<UserDto> getUsersByName(String name); List<UserDto> getUsersByName(String name);
UserDto saveFavorite(Integer userId, Integer recipeId); UserDto saveFavorite(Integer userId, Integer recipeId);
UserDto updateUser(User user, Integer Id); UserDto updateUser(User user, Integer id);
void deleteUser(Integer Id); void deleteUser(Integer id);
void deleteFavorite(Integer userId, Integer recipeId); void deleteFavorite(Integer userId, Integer recipeId);
UserDto banUser(Integer id);
UserDto unbanUser(Integer id);
UserDto makeAdmin(Integer id);
UserDto makeUser(Integer id);
} }
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Create Thyme Crunch Account</title>
<link rel="stylesheet" th:href="@{css/create-account.css}">
<link href="https://fonts.googleapis.com/css2?family=Delius+Swash+Caps&family=Mali:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700" rel="stylesheet">
</head>
<body>
<header class="top-header">
<img th:src="@{images/header_left.png}" alt="Violin f-hole shape to the left of header." class="swirl">
<h1 class="site-name">Thyme Crunch</h1>
<img th:src="@{images/header_right.png}" alt="Violin f-hole shape to the right of header." class="swirl">
</header>
<!-- Main Content -->
<main class="main-content">
<div class="login-box">
<h2>Create Account</h2>
<form id="createUserForm">
<div class="rows">
<label for="username">Username</label>
<input type="text" id="username" required>
</div>
<div class="rows">
<label for="email">Email</label>
<input type="email" id="email" required>
</div>
<div class="rows">
<label for="password">Password</label>
<input type="password" id="password" required>
</div>
<div class="rows">
<label for="confirmPassword">Confirm Password</label>
<input type="password" id="confirmPassword" required>
</div>
<p id="passwordError"></p>
<button type="submit">Create</button>
</form>
</div>
</main>
</body>
</html>
<script>
document.addEventListener("DOMContentLoaded", function () {
const passwordField = document.getElementById("password");
const confirmPasswordField = document.getElementById("confirmPassword");
function checkPasswords() {
if (confirmPasswordField.value === "") {
confirmPasswordField.classList.remove("invalid");
return;
}
if (passwordField.value !== confirmPasswordField.value) {
confirmPasswordField.classList.add("invalid");
} else {
confirmPasswordField.classList.remove("invalid");
}
}
passwordField.addEventListener("input", checkPasswords);
confirmPasswordField.addEventListener("input", checkPasswords);
document.getElementById("createUserForm").addEventListener("submit", function(e) {
const password = passwordField.value;
const confirmPassword = confirmPasswordField.value;
if (password !== confirmPassword) {
e.preventDefault();
confirmPasswordField.classList.add("invalid");
return;
}
const userData = {
username: document.getElementById("username").value,
email: document.getElementById("email").value,
hashedpassword: password,
role: "USER"
};
fetch("http://localhost:8080/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(userData)
});
e.preventDefault();
});
});
</script>