add app API endpoints for categories, platforms, and products; update security configuration

This commit is contained in:
Vincent Guillet
2025-11-01 15:44:26 +01:00
parent 7c8f85a500
commit e7da8b9b83
20 changed files with 514 additions and 8 deletions

View File

@@ -46,8 +46,7 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // autoriser les preflight .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // autoriser les preflight
.requestMatchers("/api/auth/**").permitAll() .requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/users/**").authenticated() .requestMatchers("/api/users/**").authenticated()
.requestMatchers("/api/brands/**").permitAll() .requestMatchers("/api/app/**").permitAll()
.requestMatchers("/api/platforms/**").permitAll()
.anyRequest().permitAll() .anyRequest().permitAll()
) )
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

View File

@@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
@RestController @RestController
@RequestMapping("/api/brands") @RequestMapping("/api/app/brands")
public class BrandController { public class BrandController {
private final BrandService brandService; private final BrandService brandService;

View File

@@ -0,0 +1,60 @@
package fr.gameovergne.api.controller.app;
import fr.gameovergne.api.dto.app.CategoryDTO;
import fr.gameovergne.api.mapper.app.CategoryMapper;
import fr.gameovergne.api.model.app.Category;
import fr.gameovergne.api.service.app.CategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/app/categories")
public class CategoryController {
private final CategoryService categoryService;
private final CategoryMapper categoryMapper;
@Autowired
public CategoryController(CategoryService categoryService, CategoryMapper categoryMapper) {
this.categoryService = categoryService;
this.categoryMapper = categoryMapper;
}
@GetMapping
public List<Category> getAllCategories() {
return categoryService.getAllCategories();
}
@GetMapping("/{id}")
public ResponseEntity<Category> getCategoryById(@PathVariable Long id) {
return categoryService.getCategoryById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public void saveCategory(@RequestBody CategoryDTO categoryDTO) {
categoryService.saveCategory(categoryMapper.fromDto(categoryDTO));
}
@PutMapping("/{id}")
public ResponseEntity<Category> updateCategory(@PathVariable Long id, @RequestBody CategoryDTO categoryDTO) {
Category category = categoryMapper.fromDto(categoryDTO);
category.setId(id);
return categoryService.getCategoryById(id)
.map(existingCategory -> categoryService.updateCategory(category)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build()))
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Category> deleteCategoryById(@PathVariable Long id) {
return categoryService.deleteCategoryById(id)
.map(category -> ResponseEntity.ok().body(category))
.orElse(ResponseEntity.notFound().build());
}
}

View File

@@ -0,0 +1,23 @@
package fr.gameovergne.api.controller.app;
import fr.gameovergne.api.dto.app.ConditionDTO;
import fr.gameovergne.api.model.app.Condition;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/app/conditions")
public class ConditionController {
@GetMapping
public List<ConditionDTO> list() {
return Arrays.stream(Condition.values())
.map(condition -> new ConditionDTO(condition.name(), condition.getDisplayName()))
.collect(Collectors.toList());
}
}

View File

@@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
@RestController @RestController
@RequestMapping("/api/platforms") @RequestMapping("/api/app/platforms")
public class PlatformController { public class PlatformController {
private final PlatformService platformService; private final PlatformService platformService;

View File

@@ -0,0 +1,60 @@
package fr.gameovergne.api.controller.app;
import fr.gameovergne.api.dto.app.ProductDTO;
import fr.gameovergne.api.mapper.app.ProductMapper;
import fr.gameovergne.api.model.app.Product;
import fr.gameovergne.api.service.app.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/app/products")
public class ProductController {
private final ProductService productService;
private final ProductMapper productMapper;
@Autowired
public ProductController(ProductService productService, ProductMapper productMapper) {
this.productService = productService;
this.productMapper = productMapper;
}
@GetMapping
public List<Product> getAllProducts() {
return productService.getAllProducts();
}
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
return productService.getProductById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public void saveProduct(@RequestBody ProductDTO productDTO) {
productService.saveProduct(productMapper.fromDto(productDTO));
}
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id, @RequestBody ProductDTO productDTO) {
Product product = productMapper.fromDto(productDTO);
product.setId(id);
return productService.getProductById(id)
.map(existingProduct -> productService.updateProduct(product)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build()))
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Product> deleteProductById(@PathVariable Long id) {
return productService.deleteProductById(id)
.map(product -> ResponseEntity.ok().body(product))
.orElse(ResponseEntity.notFound().build());
}
}

View File

@@ -0,0 +1,15 @@
package fr.gameovergne.api.dto.app;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CategoryDTO {
@JsonProperty("name")
private String name;
}

View File

@@ -0,0 +1,16 @@
package fr.gameovergne.api.dto.app;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class ConditionDTO {
@JsonProperty("name")
private final String name;
@JsonProperty("displayName")
private final String displayName;
}

View File

@@ -0,0 +1,39 @@
package fr.gameovergne.api.dto.app;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProductDTO {
@JsonProperty("title")
private String title;
@JsonProperty("description")
private String description;
@JsonProperty("price")
private double price;
@JsonProperty("quantity")
private int quantity;
@JsonProperty("complete")
private boolean complete = false;
@JsonProperty("manualIncluded")
private boolean manualIncluded = false;
@JsonProperty("category")
private CategoryDTO categoryDTO;
@JsonProperty("platform")
private PlatformDTO platformDTO;
@JsonProperty("condition")
private ConditionDTO conditionDTO;
}

View File

@@ -0,0 +1,23 @@
package fr.gameovergne.api.mapper.app;
import fr.gameovergne.api.dto.app.CategoryDTO;
import fr.gameovergne.api.model.app.Category;
import org.springframework.stereotype.Component;
@Component
public class CategoryMapper {
public Category fromDto(CategoryDTO categoryDTO) {
if (categoryDTO == null) return null;
Category category = new Category();
category.setName(categoryDTO.getName());
return category;
}
public CategoryDTO toDto(Category category) {
if (category == null) return null;
CategoryDTO categoryDTO = new CategoryDTO();
categoryDTO.setName(category.getName());
return categoryDTO;
}
}

View File

@@ -19,6 +19,7 @@ public class PlatformMapper {
if (platformDTO == null) return null; if (platformDTO == null) return null;
Platform platform = new Platform(); Platform platform = new Platform();
platform.setName(platformDTO.getName()); platform.setName(platformDTO.getName());
if (platformDTO.getBrandDTO() != null && platformDTO.getBrandDTO().getName() != null) { if (platformDTO.getBrandDTO() != null && platformDTO.getBrandDTO().getName() != null) {
platform.setBrand(this.brandService.getBrandByName(platformDTO.getBrandDTO().getName()).orElse(null)); platform.setBrand(this.brandService.getBrandByName(platformDTO.getBrandDTO().getName()).orElse(null));
} else { } else {

View File

@@ -0,0 +1,121 @@
package fr.gameovergne.api.mapper.app;
import fr.gameovergne.api.dto.app.*;
import fr.gameovergne.api.model.app.Brand;
import fr.gameovergne.api.model.app.Condition;
import fr.gameovergne.api.model.app.Platform;
import fr.gameovergne.api.model.app.Product;
import fr.gameovergne.api.service.app.BrandService;
import fr.gameovergne.api.service.app.CategoryService;
import fr.gameovergne.api.service.app.PlatformService;
import org.springframework.stereotype.Component;
@Component
public class ProductMapper {
private final CategoryService categoryService;
private final BrandService brandService;
private final PlatformService platformService;
public ProductMapper(CategoryService categoryService, BrandService brandService, PlatformService platformService) {
this.categoryService = categoryService;
this.brandService = brandService;
this.platformService = platformService;
}
public Product fromDto(ProductDTO productDTO) {
if (productDTO == null) return null;
Product product = new Product();
product.setTitle(productDTO.getTitle());
product.setDescription(productDTO.getDescription());
product.setPrice(productDTO.getPrice());
product.setQuantity(productDTO.getQuantity());
product.setComplete(productDTO.isComplete());
product.setManualIncluded(productDTO.isManualIncluded());
if (productDTO.getCategoryDTO() != null && productDTO.getCategoryDTO().getName() != null) {
product.setCategory(this.categoryService.getCategoryByName(productDTO.getCategoryDTO().getName()).orElse(null));
} else {
product.setCategory(null);
}
// Platform + Brand handling — ensure platform is set on product
if (productDTO.getPlatformDTO() != null && productDTO.getPlatformDTO().getName() != null) {
String platformName = productDTO.getPlatformDTO().getName();
Platform platform = this.platformService.getPlatformByName(platformName)
.orElseGet(() -> {
Platform p = new Platform();
p.setName(platformName);
return p;
});
if (productDTO.getPlatformDTO().getBrandDTO() != null && productDTO.getPlatformDTO().getBrandDTO().getName() != null) {
String brandName = productDTO.getPlatformDTO().getBrandDTO().getName();
Brand brand = this.brandService.getBrandByName(brandName)
.orElseGet(() -> {
Brand b = new Brand();
b.setName(brandName);
return b;
});
platform.setBrand(brand);
}
product.setPlatform(platform);
} else {
product.setPlatform(null);
}
// Condition handling with null/invalid protection
if (productDTO.getConditionDTO() != null && productDTO.getConditionDTO().getName() != null) {
try {
product.setCondition(Condition.valueOf(productDTO.getConditionDTO().getName()));
} catch (IllegalArgumentException e) {
product.setCondition(null);
}
} else {
product.setCondition(null);
}
return product;
}
public ProductDTO toDto(Product product) {
if (product == null) return null;
ProductDTO productDTO = new ProductDTO();
productDTO.setTitle(product.getTitle());
productDTO.setDescription(product.getDescription());
productDTO.setPrice(product.getPrice());
productDTO.setQuantity(product.getQuantity());
productDTO.setComplete(product.isComplete());
productDTO.setManualIncluded(product.isManualIncluded());
if (product.getCategory() != null) {
productDTO.setCategoryDTO(new CategoryDTO(product.getCategory().getName()));
} else {
productDTO.setCategoryDTO(null);
}
if (product.getPlatform() != null) {
Platform platform = product.getPlatform();
PlatformDTO platformDTO = new PlatformDTO();
platformDTO.setName(platform.getName());
if (platform.getBrand() != null) {
platformDTO.setBrandDTO(new BrandDTO(platform.getBrand().getName()));
} else {
platformDTO.setBrandDTO(null);
}
productDTO.setPlatformDTO(platformDTO);
} else {
productDTO.setPlatformDTO(null);
}
if (product.getCondition() != null) {
productDTO.setConditionDTO(new ConditionDTO(product.getCondition().name(), product.getCondition().getDisplayName()));
} else {
productDTO.setConditionDTO(null);
}
return productDTO;
}
}

View File

@@ -22,7 +22,7 @@ public class Category {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private long id; private Long id;
@NotBlank @NotBlank
@Column(name = "category_name", length = 30, unique = true, nullable = false) @Column(name = "category_name", length = 30, unique = true, nullable = false)

View File

@@ -32,13 +32,21 @@ public class Product {
@Column(name = "product_description", length = 500) @Column(name = "product_description", length = 500)
private String description; private String description;
@NotNull
@Column(name = "product_price")
private double price;
@NotNull
@Column(name = "product_quantity")
private int quantity;
@NotNull @NotNull
@Column(name = "product_complete") @Column(name = "product_complete")
private boolean complete = false; private boolean complete = false;
@NotNull @NotNull
@Column(name = "product_manual") @Column(name = "product_manual")
private boolean manual = false; // Notice private boolean manualIncluded = false; // Notice
@ManyToOne @ManyToOne
@JoinColumn(name = "category_id") @JoinColumn(name = "category_id")

View File

@@ -6,6 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional; import java.util.Optional;
public interface BrandRepository extends JpaRepository<Brand, Long> { public interface BrandRepository extends JpaRepository<Brand, Long> {
Optional<Brand> findById(Long id);
Optional<Brand> findByName(String name); Optional<Brand> findByName(String name);
} }

View File

@@ -0,0 +1,10 @@
package fr.gameovergne.api.repository.app;
import fr.gameovergne.api.model.app.Category;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface CategoryRepository extends JpaRepository<Category, Long> {
Optional<Category> findByName(String name);
}

View File

@@ -6,6 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional; import java.util.Optional;
public interface PlatformRepository extends JpaRepository<Platform, Long> { public interface PlatformRepository extends JpaRepository<Platform, Long> {
Optional<Platform> findById(Long id);
Optional<Platform> findByName(String name); Optional<Platform> findByName(String name);
} }

View File

@@ -0,0 +1,13 @@
package fr.gameovergne.api.repository.app;
import fr.gameovergne.api.model.app.*;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface ProductRepository extends JpaRepository<Product, Long> {
Optional<Product> findByTitle(String title);
Optional<Product> findByCategory(Category category);
Optional<Product> findByPlatform(Platform platform);
Optional<Product> findByCondition(Condition condition);
}

View File

@@ -0,0 +1,55 @@
package fr.gameovergne.api.service.app;
import fr.gameovergne.api.model.app.Category;
import fr.gameovergne.api.repository.app.CategoryRepository;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class CategoryService {
private final CategoryRepository categoryRepository;
@Autowired
public CategoryService(CategoryRepository categoryRepository) {
this.categoryRepository = categoryRepository;
}
@Transactional
public void saveCategory(Category category) {
if (category.getId() == null) {
categoryRepository.save(category);
}
}
public List<Category> getAllCategories() {
return categoryRepository.findAll();
}
public Optional<Category> getCategoryById(Long id) {
return categoryRepository.findById(id);
}
public Optional<Category> getCategoryByName(String name) {
return categoryRepository.findByName(name);
}
@Transactional
public Optional<Category> updateCategory(Category category) {
return categoryRepository.findById(category.getId()).map(existingCategory -> {
existingCategory.setName(category.getName());
return categoryRepository.save(existingCategory);
});
}
@Transactional
public Optional<Category> deleteCategoryById(Long id) {
Optional<Category> category = categoryRepository.findById(id);
category.ifPresent(categoryRepository::delete);
return category;
}
}

View File

@@ -0,0 +1,65 @@
package fr.gameovergne.api.service.app;
import fr.gameovergne.api.model.app.Product;
import fr.gameovergne.api.model.app.Platform;
import fr.gameovergne.api.repository.app.ProductRepository;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class ProductService {
private final ProductRepository productRepository;
@Autowired
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Transactional
public void saveProduct(Product product) {
if (product.getId() == null) {
productRepository.save(product);
}
}
public List<Product> getAllProducts() {
return productRepository.findAll();
}
public Optional<Product> getProductById(Long id) {
return productRepository.findById(id);
}
public Optional<Product> getProductByName(String name) {
return productRepository.findByTitle(name);
}
@Transactional
public Optional<Product> updateProduct(Product product) {
return productRepository.findById(product.getId()).map(existingProduct -> {
existingProduct.setTitle(product.getTitle());
existingProduct.setDescription(product.getDescription());
existingProduct.setPrice(product.getPrice());
existingProduct.setQuantity(product.getQuantity());
existingProduct.setComplete(product.isComplete());
existingProduct.setManualIncluded(product.isManualIncluded());
existingProduct.setCategory(product.getCategory());
existingProduct.setPlatform(product.getPlatform());
existingProduct.setCondition(product.getCondition());
return productRepository.save(existingProduct);
});
}
@Transactional
public Optional<Product> deleteProductById(Long id) {
Optional<Product> product = productRepository.findById(id);
product.ifPresent(productRepository::delete);
return product;
}
}