From d8fafb1da6c90b10360e4fad3242f2458e0c7e8b Mon Sep 17 00:00:00 2001 From: Benjamin POCHAT Date: Sun, 4 Feb 2024 20:12:07 +0100 Subject: [PATCH] add authentication step at customer order form --- .../repository/ProductionRepository.java | 2 +- .../configuration/SecurityConfiguration.java | 17 +- .../service/AddresseService.java | 4 +- .../AuthenticationProducerService.java | 33 +++ .../service/BeefProductionService.java | 6 +- .../service/CustomerService.java | 4 +- .../service/ProducerService.java | 51 +++- .../service/ProductionService.java | 4 +- .../viandeendirect/service/SaleService.java | 28 +-- ...> AuthenticationProducerServiceSpecs.java} | 2 +- ...uthenticationProducerServiceForTests.java} | 4 +- .../service/TestBeefProductionService.java | 5 - .../service/TestProducerService.java | 91 +++++++ .../service/TestSaleService.java | 39 +-- .../eu/viandeendirect/model/PackageLot.java | 3 + frontend/app/package-lock.json | 45 ++++ frontend/app/package.json | 1 + frontend/app/src/App.js | 14 +- frontend/app/src/api/mock/MockApi.ts | 10 + frontend/app/src/api/mock/MockApiProducers.ts | 223 ++++++++++++++++++ frontend/app/src/api/mock/MockApiSales.ts | 143 ----------- .../authentication/AuthenticationService.ts | 15 +- .../components/BeefProductionCustomerCard.tsx | 2 +- .../app/src/domains/sale/SaleController.tsx | 8 +- .../domains/sale/views/CustomerOrderForm.tsx | 88 +++++-- .../sale/views/{SaleForm.js => SaleForm.tsx} | 6 +- .../app/src/domains/sale/views/SalesList.tsx | 4 +- .../src/layouts/customer/CustomerLayout.tsx | 12 +- ...AnonymousLayout.js => AnonymousLayout.tsx} | 0 ...catedLayout.js => AuthenticatedLayout.tsx} | 19 +- .../app/src/layouts/producer/LayoutWrapper.js | 4 +- openapi/openapi.yml | 57 ++++- 32 files changed, 665 insertions(+), 279 deletions(-) create mode 100644 backend/app/src/main/java/eu/viandeendirect/service/AuthenticationProducerService.java rename backend/app/src/main/java/eu/viandeendirect/service/specs/{ProducerServiceSpecs.java => AuthenticationProducerServiceSpecs.java} (70%) rename backend/app/src/test/java/eu/viandeendirect/service/{ProducerServiceForTests.java => AuthenticationProducerServiceForTests.java} (68%) create mode 100644 backend/app/src/test/java/eu/viandeendirect/service/TestProducerService.java create mode 100644 frontend/app/src/api/mock/MockApiProducers.ts rename frontend/app/src/domains/sale/views/{SaleForm.js => SaleForm.tsx} (98%) rename frontend/app/src/layouts/producer/{AnonymousLayout.js => AnonymousLayout.tsx} (100%) rename frontend/app/src/layouts/producer/{AuthenticatedLayout.js => AuthenticatedLayout.tsx} (80%) diff --git a/backend/app/src/main/java/eu/viandeendirect/repository/ProductionRepository.java b/backend/app/src/main/java/eu/viandeendirect/repository/ProductionRepository.java index 7bbb21f..e5baa35 100644 --- a/backend/app/src/main/java/eu/viandeendirect/repository/ProductionRepository.java +++ b/backend/app/src/main/java/eu/viandeendirect/repository/ProductionRepository.java @@ -13,5 +13,5 @@ public interface ProductionRepository extends CrudRepository findByProducer(@Param("producer") Producer producer); @Query("select p from Sale s inner join s.productions p where s.id = :saleId") - List findBySalesId(@Param("saleId") Integer saleId); + List findBySaleId(@Param("saleId") Integer saleId); } \ No newline at end of file diff --git a/backend/app/src/main/java/eu/viandeendirect/security/configuration/SecurityConfiguration.java b/backend/app/src/main/java/eu/viandeendirect/security/configuration/SecurityConfiguration.java index ec040f7..1c60e8d 100644 --- a/backend/app/src/main/java/eu/viandeendirect/security/configuration/SecurityConfiguration.java +++ b/backend/app/src/main/java/eu/viandeendirect/security/configuration/SecurityConfiguration.java @@ -5,6 +5,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.session.SessionRegistryImpl; @@ -38,7 +39,11 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests() .requestMatchers("/swagger-ui") - .anonymous() + .permitAll() + .and() + .authorizeHttpRequests() + .requestMatchers("/addresses", "/addresses/**") + .permitAll() .and() .authorizeHttpRequests() .requestMatchers("/sales", "/sales/**") @@ -50,11 +55,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .and() .authorizeHttpRequests() .requestMatchers("/productions", "/productions/**") - .hasRole("PRODUCER") + //.hasRole("PRODUCER") + .permitAll() .and() .authorizeHttpRequests() .requestMatchers("/beefProductions", "/beefProductions/**") - .hasRole("PRODUCER") + //.hasRole("PRODUCER") + .permitAll() .and() .authorizeHttpRequests() .requestMatchers("/honneyProductions", "/honneyProductions/**") @@ -63,6 +70,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests() .requestMatchers("/customers", "/customers/**") .hasRole("PRODUCER") + .and() + .authorizeHttpRequests() + .requestMatchers("/producers", "/producers/**") + .hasRole("PRODUCER") .and() .authorizeHttpRequests() .requestMatchers("/orders", "/orders/**") diff --git a/backend/app/src/main/java/eu/viandeendirect/service/AddresseService.java b/backend/app/src/main/java/eu/viandeendirect/service/AddresseService.java index 901e204..9b54f18 100644 --- a/backend/app/src/main/java/eu/viandeendirect/service/AddresseService.java +++ b/backend/app/src/main/java/eu/viandeendirect/service/AddresseService.java @@ -4,7 +4,7 @@ import eu.viandeendirect.model.Address; import eu.viandeendirect.model.Producer; import eu.viandeendirect.repository.AddressRepository; -import eu.viandeendirect.service.specs.ProducerServiceSpecs; +import eu.viandeendirect.service.specs.AuthenticationProducerServiceSpecs; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -19,7 +19,7 @@ public class AddresseService implements AddressesApiDelegate { AddressRepository addressRepository; @Autowired - ProducerServiceSpecs producerService; + AuthenticationProducerServiceSpecs producerService; @Override public ResponseEntity> getAddresses() { diff --git a/backend/app/src/main/java/eu/viandeendirect/service/AuthenticationProducerService.java b/backend/app/src/main/java/eu/viandeendirect/service/AuthenticationProducerService.java new file mode 100644 index 0000000..71f0739 --- /dev/null +++ b/backend/app/src/main/java/eu/viandeendirect/service/AuthenticationProducerService.java @@ -0,0 +1,33 @@ +package eu.viandeendirect.service; + +import eu.viandeendirect.api.ProducersApiDelegate; +import eu.viandeendirect.model.Producer; +import eu.viandeendirect.model.Sale; +import eu.viandeendirect.repository.ProducerRepository; +import eu.viandeendirect.repository.SaleRepository; +import eu.viandeendirect.service.specs.AuthenticationProducerServiceSpecs; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Service; + +import static org.springframework.http.HttpStatus.CREATED; + +@Service +@Profile("!test") +public class AuthenticationProducerService implements AuthenticationProducerServiceSpecs { + + @Autowired + ProducerRepository producerRepository; + + @Override + public Producer getAuthenticatedProducer() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = ((JwtAuthenticationToken)authentication).getToken().getClaimAsString("email"); + Producer producer = producerRepository.findByEmail(email).orElseThrow(); + return producer; + } +} diff --git a/backend/app/src/main/java/eu/viandeendirect/service/BeefProductionService.java b/backend/app/src/main/java/eu/viandeendirect/service/BeefProductionService.java index 186aaad..408479f 100644 --- a/backend/app/src/main/java/eu/viandeendirect/service/BeefProductionService.java +++ b/backend/app/src/main/java/eu/viandeendirect/service/BeefProductionService.java @@ -2,10 +2,8 @@ import eu.viandeendirect.api.BeefProductionsApiDelegate; import eu.viandeendirect.model.BeefProduction; -import eu.viandeendirect.model.Producer; -import eu.viandeendirect.model.Production; import eu.viandeendirect.repository.ProductionRepository; -import eu.viandeendirect.service.specs.ProducerServiceSpecs; +import eu.viandeendirect.service.specs.AuthenticationProducerServiceSpecs; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -18,7 +16,7 @@ public class BeefProductionService implements BeefProductionsApiDelegate { ProductionRepository productionRepository; @Autowired - ProducerServiceSpecs producerService; + AuthenticationProducerServiceSpecs producerService; @Override public ResponseEntity getBeefProduction(Integer beefProductionId) { diff --git a/backend/app/src/main/java/eu/viandeendirect/service/CustomerService.java b/backend/app/src/main/java/eu/viandeendirect/service/CustomerService.java index 5ed47e8..70b0e2c 100644 --- a/backend/app/src/main/java/eu/viandeendirect/service/CustomerService.java +++ b/backend/app/src/main/java/eu/viandeendirect/service/CustomerService.java @@ -5,7 +5,7 @@ import eu.viandeendirect.model.Producer; import eu.viandeendirect.repository.CustomerRepository; import eu.viandeendirect.repository.UserRepository; -import eu.viandeendirect.service.specs.ProducerServiceSpecs; +import eu.viandeendirect.service.specs.AuthenticationProducerServiceSpecs; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -23,7 +23,7 @@ public class CustomerService implements CustomersApiDelegate { UserRepository userRepository; @Autowired - ProducerServiceSpecs producerService; + AuthenticationProducerServiceSpecs producerService; @Override diff --git a/backend/app/src/main/java/eu/viandeendirect/service/ProducerService.java b/backend/app/src/main/java/eu/viandeendirect/service/ProducerService.java index e806af0..124a929 100644 --- a/backend/app/src/main/java/eu/viandeendirect/service/ProducerService.java +++ b/backend/app/src/main/java/eu/viandeendirect/service/ProducerService.java @@ -1,27 +1,56 @@ package eu.viandeendirect.service; +import eu.viandeendirect.api.ProducersApiDelegate; import eu.viandeendirect.model.Producer; +import eu.viandeendirect.model.Sale; import eu.viandeendirect.repository.ProducerRepository; -import eu.viandeendirect.service.specs.ProducerServiceSpecs; +import eu.viandeendirect.repository.SaleRepository; +import eu.viandeendirect.service.specs.AuthenticationProducerServiceSpecs; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Profile; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; + +import static org.springframework.http.HttpStatus.*; + @Service -@Profile("!test") -public class ProducerService implements ProducerServiceSpecs { +public class ProducerService implements ProducersApiDelegate { @Autowired ProducerRepository producerRepository; + @Autowired + SaleRepository saleRepository; + + @Autowired + AuthenticationProducerServiceSpecs producerService; + @Override - public Producer getAuthenticatedProducer() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String email = ((JwtAuthenticationToken)authentication).getToken().getClaimAsString("email"); + public ResponseEntity createProducerSale(Integer producerId, Sale sale) { + Producer producer = producerService.getAuthenticatedProducer(); + if (!producer.getId().equals(producerId)) { + return new ResponseEntity<>(FORBIDDEN); + } + sale.setSeller(producer); + return new ResponseEntity<>(saleRepository.save(sale), CREATED); + } + + @Override + public ResponseEntity getProducer(String email) { Producer producer = producerRepository.findByEmail(email).orElseThrow(); - return producer; + return new ResponseEntity<>(producer, OK); + } + + @Override + public ResponseEntity> getProducerSales(Integer producerId) { + Producer producer = producerService.getAuthenticatedProducer(); + if (!producer.getId().equals(producerId)) { + return new ResponseEntity<>(FORBIDDEN); + } + List sales = new ArrayList<>(); + saleRepository.findBySeller(producer).forEach(sales::add);; + return new ResponseEntity<>(sales, OK); } } diff --git a/backend/app/src/main/java/eu/viandeendirect/service/ProductionService.java b/backend/app/src/main/java/eu/viandeendirect/service/ProductionService.java index 10d84c9..2450c6f 100644 --- a/backend/app/src/main/java/eu/viandeendirect/service/ProductionService.java +++ b/backend/app/src/main/java/eu/viandeendirect/service/ProductionService.java @@ -6,7 +6,7 @@ import eu.viandeendirect.model.Production; import eu.viandeendirect.repository.PackageLotRepository; import eu.viandeendirect.repository.ProductionRepository; -import eu.viandeendirect.service.specs.ProducerServiceSpecs; +import eu.viandeendirect.service.specs.AuthenticationProducerServiceSpecs; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -25,7 +25,7 @@ public class ProductionService implements ProductionsApiDelegate { PackageLotRepository packageLotRepository; @Autowired - ProducerServiceSpecs producerService; + AuthenticationProducerServiceSpecs producerService; @Override public ResponseEntity> getProductions(Boolean forSale) { diff --git a/backend/app/src/main/java/eu/viandeendirect/service/SaleService.java b/backend/app/src/main/java/eu/viandeendirect/service/SaleService.java index 15c10f3..a864de9 100644 --- a/backend/app/src/main/java/eu/viandeendirect/service/SaleService.java +++ b/backend/app/src/main/java/eu/viandeendirect/service/SaleService.java @@ -5,7 +5,7 @@ import eu.viandeendirect.repository.OrderRepository; import eu.viandeendirect.repository.ProductionRepository; import eu.viandeendirect.repository.SaleRepository; -import eu.viandeendirect.service.specs.ProducerServiceSpecs; +import eu.viandeendirect.service.specs.AuthenticationProducerServiceSpecs; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.List; -import static org.springframework.http.HttpStatus.CREATED; import static org.springframework.http.HttpStatus.OK; @Service @@ -23,33 +22,26 @@ public class SaleService implements SalesApiDelegate { SaleRepository saleRepository; @Autowired - ProducerServiceSpecs producerService; + AuthenticationProducerServiceSpecs producerService; @Autowired private OrderRepository orderRepository; @Autowired private ProductionRepository productionRepository; - @Override - public ResponseEntity> getSales() { - Producer producer = producerService.getAuthenticatedProducer(); - List sales = new ArrayList<>(); - saleRepository.findBySeller(producer).forEach(sales::add);; - return new ResponseEntity<>(sales, OK); - } - - @Override - public ResponseEntity createSale(Sale sale) { - sale.setSeller(producerService.getAuthenticatedProducer()); - return new ResponseEntity<>(saleRepository.save(sale), CREATED); - } - @Override public ResponseEntity getSale(Integer saleId) { Sale sale = saleRepository.findById(saleId).get(); return new ResponseEntity<>(sale, OK); } + @Override + public ResponseEntity> getSales() { + List sales = new ArrayList<>(); + saleRepository.findAll().forEach(sales::add); + return new ResponseEntity<>(sales, OK); + } + @Override public ResponseEntity> getSaleOrders(Integer saleId) { return new ResponseEntity<>(orderRepository.findBySaleId(saleId), OK); @@ -57,6 +49,6 @@ public ResponseEntity> getSaleOrders(Integer saleId) { @Override public ResponseEntity> getSaleProductions(Integer saleId) { - return new ResponseEntity<>(productionRepository.findBySalesId(saleId), OK); + return new ResponseEntity<>(productionRepository.findBySaleId(saleId), OK); } } diff --git a/backend/app/src/main/java/eu/viandeendirect/service/specs/ProducerServiceSpecs.java b/backend/app/src/main/java/eu/viandeendirect/service/specs/AuthenticationProducerServiceSpecs.java similarity index 70% rename from backend/app/src/main/java/eu/viandeendirect/service/specs/ProducerServiceSpecs.java rename to backend/app/src/main/java/eu/viandeendirect/service/specs/AuthenticationProducerServiceSpecs.java index 13805c3..13132a3 100644 --- a/backend/app/src/main/java/eu/viandeendirect/service/specs/ProducerServiceSpecs.java +++ b/backend/app/src/main/java/eu/viandeendirect/service/specs/AuthenticationProducerServiceSpecs.java @@ -2,6 +2,6 @@ import eu.viandeendirect.model.Producer; -public interface ProducerServiceSpecs { +public interface AuthenticationProducerServiceSpecs { Producer getAuthenticatedProducer(); } diff --git a/backend/app/src/test/java/eu/viandeendirect/service/ProducerServiceForTests.java b/backend/app/src/test/java/eu/viandeendirect/service/AuthenticationProducerServiceForTests.java similarity index 68% rename from backend/app/src/test/java/eu/viandeendirect/service/ProducerServiceForTests.java rename to backend/app/src/test/java/eu/viandeendirect/service/AuthenticationProducerServiceForTests.java index 0302305..79259d4 100644 --- a/backend/app/src/test/java/eu/viandeendirect/service/ProducerServiceForTests.java +++ b/backend/app/src/test/java/eu/viandeendirect/service/AuthenticationProducerServiceForTests.java @@ -1,13 +1,13 @@ package eu.viandeendirect.service; import eu.viandeendirect.model.Producer; -import eu.viandeendirect.service.specs.ProducerServiceSpecs; +import eu.viandeendirect.service.specs.AuthenticationProducerServiceSpecs; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @Service @Profile("test") -public class ProducerServiceForTests implements ProducerServiceSpecs { +public class AuthenticationProducerServiceForTests implements AuthenticationProducerServiceSpecs { @Override public Producer getAuthenticatedProducer() { diff --git a/backend/app/src/test/java/eu/viandeendirect/service/TestBeefProductionService.java b/backend/app/src/test/java/eu/viandeendirect/service/TestBeefProductionService.java index 1d2c956..c96e91d 100644 --- a/backend/app/src/test/java/eu/viandeendirect/service/TestBeefProductionService.java +++ b/backend/app/src/test/java/eu/viandeendirect/service/TestBeefProductionService.java @@ -3,8 +3,6 @@ import eu.viandeendirect.model.BeefProduction; import eu.viandeendirect.model.Production; import eu.viandeendirect.repository.ProductionRepository; -import eu.viandeendirect.service.specs.ProducerServiceSpecs; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -14,9 +12,6 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; -import java.util.Optional; - -import static eu.viandeendirect.model.Production.ProductionTypeEnum.BEEFPRODUCTION; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD; import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_METHOD; diff --git a/backend/app/src/test/java/eu/viandeendirect/service/TestProducerService.java b/backend/app/src/test/java/eu/viandeendirect/service/TestProducerService.java new file mode 100644 index 0000000..97b3ab4 --- /dev/null +++ b/backend/app/src/test/java/eu/viandeendirect/service/TestProducerService.java @@ -0,0 +1,91 @@ +package eu.viandeendirect.service; + +import eu.viandeendirect.model.Producer; +import eu.viandeendirect.model.Sale; +import eu.viandeendirect.repository.SaleRepository; +import eu.viandeendirect.service.specs.AuthenticationProducerServiceSpecs; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatusCode; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD; +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_METHOD; + +@SpringBootTest +@ActiveProfiles(value = "test") +@ExtendWith({SpringExtension.class}) +@Sql(value = {"/sql/create_test_data.sql"}, executionPhase = BEFORE_TEST_METHOD) +@Sql(value = {"/sql/delete_test_data.sql"}, executionPhase = AFTER_TEST_METHOD) +public class TestProducerService { + + @Autowired + AuthenticationProducerServiceSpecs authenticationProducerService; + + @Autowired + SaleRepository saleRepository; + + @Autowired + ProducerService producerService; + + @Test + void getProducerSales_should_return_all_sales_for_the_current_producer() { + // given + Producer producer = authenticationProducerService.getAuthenticatedProducer(); + + // when + List sales = producerService.getProducerSales(producer.getId()).getBody(); + + // then + assertThat(sales) + .hasSize(1) + .anyMatch(sale -> sale.getId().equals(1000)); + } + + @Test + void getProducerSales_should_raise_an_error_if_accessing_sales_of_another_producer() { + // when + HttpStatusCode status = producerService.getProducerSales(-1).getStatusCode(); + + // then + assertThat(status).isEqualTo(HttpStatusCode.valueOf(403)); + } + + @Test + void createProducerSale_should_persist_a_new_sale_in_database() { + // given + Producer producer = authenticationProducerService.getAuthenticatedProducer(); + + Sale sale = new Sale(); + sale.setDeliveryCity("Chamonix"); + sale.setDeliveryAddressLine1("10 rue du tunnel"); + + // when + Sale saleCreated = producerService.createProducerSale(producer.getId(), sale).getBody(); + + // then + assertThat(saleCreated).isNotNull(); + assertThat(saleCreated.getId()).isNotNull(); + Sale saleReloaded = saleRepository.findById(saleCreated.getId()).get(); + assertThat(saleReloaded).isNotNull(); + assertThat(saleReloaded.getDeliveryCity()).isEqualTo("Chamonix"); + assertThat(saleReloaded.getDeliveryAddressLine1()).isEqualTo("10 rue du tunnel"); + assertThat(saleReloaded.getSeller().getId()).isEqualTo(1000); + } + + @Test + void createProducerSale_should_raise_an_error_if_creating_a_sale_for_another_producer() { + // when / then + HttpStatusCode status = producerService.createProducerSale(-1, new Sale()).getStatusCode(); + + // then + assertThat(status).isEqualTo(HttpStatusCode.valueOf(403)); + } +} diff --git a/backend/app/src/test/java/eu/viandeendirect/service/TestSaleService.java b/backend/app/src/test/java/eu/viandeendirect/service/TestSaleService.java index 321e078..3110f81 100644 --- a/backend/app/src/test/java/eu/viandeendirect/service/TestSaleService.java +++ b/backend/app/src/test/java/eu/viandeendirect/service/TestSaleService.java @@ -5,7 +5,7 @@ import eu.viandeendirect.model.Production; import eu.viandeendirect.model.Sale; import eu.viandeendirect.repository.SaleRepository; -import eu.viandeendirect.service.specs.ProducerServiceSpecs; +import eu.viandeendirect.service.specs.AuthenticationProducerServiceSpecs; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -31,32 +31,6 @@ public class TestSaleService { @Autowired SaleService saleService; - @Autowired - ProducerServiceSpecs producerService; - - @Autowired - SaleRepository saleRepository; - - @Test - void createSale_should_persist_a_new_sale_in_database() { - // given - Sale sale = new Sale(); - sale.setDeliveryCity("Chamonix"); - sale.setDeliveryAddressLine1("10 rue du tunnel"); - - // when - Sale saleCreated = saleService.createSale(sale).getBody(); - - // then - assertThat(saleCreated).isNotNull(); - assertThat(saleCreated.getId()).isNotNull(); - Sale saleReloaded = saleRepository.findById(saleCreated.getId()).get(); - assertThat(saleReloaded).isNotNull(); - assertThat(saleReloaded.getDeliveryCity()).isEqualTo("Chamonix"); - assertThat(saleReloaded.getDeliveryAddressLine1()).isEqualTo("10 rue du tunnel"); - assertThat(saleReloaded.getSeller().getId()).isEqualTo(1000); - } - @Test void getSale_should_return_the_right_sale() { // when @@ -68,17 +42,6 @@ void getSale_should_return_the_right_sale() { assertThat(sale.getSeller().getId()).isEqualTo(2000); } - @Test - void getSales_should_return_all_sales_for_the_current_producer() { - // when - List sales = saleService.getSales().getBody(); - - // then - assertThat(sales) - .hasSize(1) - .anyMatch(sale -> sale.getId().equals(1000)); - } - @Test void getSaleOrders_should_return_all_orders_for_the_given_sale() { // when diff --git a/backend/model/src/main/java/eu/viandeendirect/model/PackageLot.java b/backend/model/src/main/java/eu/viandeendirect/model/PackageLot.java index 0d75eec..aee818b 100644 --- a/backend/model/src/main/java/eu/viandeendirect/model/PackageLot.java +++ b/backend/model/src/main/java/eu/viandeendirect/model/PackageLot.java @@ -2,6 +2,8 @@ import java.net.URI; import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonCreator; import eu.viandeendirect.model.Production; @@ -33,6 +35,7 @@ public class PackageLot { @JsonProperty("production") @ManyToOne + @JsonIgnore private Production production; @JsonProperty("label") diff --git a/frontend/app/package-lock.json b/frontend/app/package-lock.json index 0b5be82..4b0842b 100644 --- a/frontend/app/package-lock.json +++ b/frontend/app/package-lock.json @@ -22,6 +22,7 @@ "querystring": "^0.2.1", "querystring-es3": "^0.2.1", "react": "^18.2.0", + "react-cookie": "^7.0.2", "react-dom": "^18.2.0", "react-hook-form": "^7.46.1", "react-hook-form-mui": "^6.5.2", @@ -4805,6 +4806,11 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, "node_modules/@types/eslint": { "version": "8.44.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", @@ -4858,6 +4864,15 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/html-minifier-terser": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", @@ -15541,6 +15556,19 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/react-cookie": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-7.0.2.tgz", + "integrity": "sha512-UnW1rZw1VibRdTvV8Ksr0BKKZoajeUxYLE89sIygDeyQgtz6ik89RHOM+3kib36G9M7HxheORggPoLk5DxAK7Q==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.5", + "hoist-non-react-statics": "^3.3.2", + "universal-cookie": "^7.0.0" + }, + "peerDependencies": { + "react": ">= 16.3.0" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -17894,6 +17922,23 @@ "node": ">=8" } }, + "node_modules/universal-cookie": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-7.0.2.tgz", + "integrity": "sha512-EC9PA+1nojhJtVnKW2Z7WYah01jgYJApqhX+Y8XU97TnFd7KaoxWTHiTZFtfpfV50jEF1L8V5p64ZxIx3Q67dg==", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0" + } + }, + "node_modules/universal-cookie/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", diff --git a/frontend/app/package.json b/frontend/app/package.json index e33cd73..777ff64 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -17,6 +17,7 @@ "querystring": "^0.2.1", "querystring-es3": "^0.2.1", "react": "^18.2.0", + "react-cookie": "^7.0.2", "react-dom": "^18.2.0", "react-hook-form": "^7.46.1", "react-hook-form-mui": "^6.5.2", diff --git a/frontend/app/src/App.js b/frontend/app/src/App.js index 7113e62..b30da6c 100644 --- a/frontend/app/src/App.js +++ b/frontend/app/src/App.js @@ -5,6 +5,8 @@ import { frFR } from '@mui/material/locale'; import { ReactKeycloakProvider } from '@react-keycloak/web' import Keycloak from 'keycloak-js' +import { CookiesProvider } from 'react-cookie'; + import ProducerLayoutWrapper from './layouts/producer/LayoutWrapper'; import CustomerLayout from './layouts/customer/CustomerLayout.tsx'; @@ -64,11 +66,13 @@ function App() { } return ( - - - {getLayoutWrapper()} - - + + + + {getLayoutWrapper()} + + + ) } export default App \ No newline at end of file diff --git a/frontend/app/src/api/mock/MockApi.ts b/frontend/app/src/api/mock/MockApi.ts index cac4d18..6f57f52 100644 --- a/frontend/app/src/api/mock/MockApi.ts +++ b/frontend/app/src/api/mock/MockApi.ts @@ -2,6 +2,7 @@ import { MockApiAddresses } from "./MockApiAddresses.ts" import { MockApiCustomers } from "./MockApiCustomers.ts" import { MockApiProductions } from "./MockApiProductions.ts" import { MockApiSales } from "./MockApiSales.ts" +import { MockApiProducers } from "./MockApiProducers.ts" export class MockApi { @@ -9,6 +10,7 @@ export class MockApi { mockApiAddresses: MockApiAddresses = new MockApiAddresses() mockApiCustomers: MockApiCustomers = new MockApiCustomers() mockApiSales: MockApiSales = new MockApiSales() + mockApiProducers: MockApiProducers = new MockApiProducers() createBeefProduction(beefProduction, callback) { callback() @@ -43,6 +45,10 @@ export class MockApi { callback(undefined, this.mockApiCustomers.createCustomer(customer)) } + getProducerSales(options, callback) { + callback(undefined, this.mockApiProducers.getProducerSales()) + } + getSales(callback) { callback(undefined, this.mockApiSales.getSales()) } @@ -62,4 +68,8 @@ export class MockApi { createOrder(order, callback) { callback(undefined, this.mockApiSales.createOrder(order)) } + + getProducer(id, callback) { + callback(undefined, this.mockApiProducers.getProducer()) + } } \ No newline at end of file diff --git a/frontend/app/src/api/mock/MockApiProducers.ts b/frontend/app/src/api/mock/MockApiProducers.ts new file mode 100644 index 0000000..bbecc72 --- /dev/null +++ b/frontend/app/src/api/mock/MockApiProducers.ts @@ -0,0 +1,223 @@ +import Producer from "viandeendirect_eu/dist/model/Producer" +import ProducerStatus from "viandeendirect_eu/dist/model/ProducerStatus" + +export class MockApiProducers { + + getProducer(): Producer { + return { + id: 1, + user: undefined, + status: 'ACTIVE', + salesCredits: undefined, + sales: undefined, + productions: undefined + } + } + + getProducerSales(): Array { + const sale1 = { + id: 1, + deliveryStart: '2023-11-15T18:00:00', + deliveryStop: '2023-11-15T20:00:00', + deliveryAddressName: 'ESL Rémilly', + deliveryAddressLine1: '1 rue De Gaulle', + deliveryAddressLine2: undefined, + deliveryCity: 'Rémilly', + deliveryZipCode: '57580', + productions: [ + { + id: 1, + productionType: 'BeefProduction', + slaughterDate: '2023-10-01T10:00:00', + animalLiveWeight: 450, + animalType: 'BEEF_HEIFER', + animalIdentifier: '9876' + } + ], + orders: [ + { + id: 11, + items: [ + { + id: 111, + unitPrice: 20, + quantity: 2, + packageLot: { + id: 111, + netWeight: 0.5 + } + }, { + id: 112, + unitPrice: 160, + quantity: 1, + packageLot: { + id: 112, + netWeight: 10 + } + } + ] + }, + { + id: 12, + items: [ + { + id: 121, + unitPrice: 160, + quantity: 1, + packageLot: { + id: 121, + netWeight: 10 + } + } + ] + } + ] + } + const sale2 = { + id: 2, + deliveryStart: '2023-11-20T16:00:00', + deliveryStop: '2023-11-20T18:00:00', + deliveryAddressName: 'Place de l\'Etoile', + deliveryAddressLine1: '1 place de l\'Etoile', + deliveryAddressLine2: 'Derrière l\'Arc de Triomphe', + deliveryCity: 'Paris', + deliveryZipCode: '75000', + productions: [ + { + id: 2, + productionType: 'BeefProduction', + slaughterDate: '2023-11-01T10:00:00', + animalLiveWeight: 400, + animalType: 'BEEF_COW', + animalIdentifier: '0987' + } + ], + orders: [ + { + id: 21, + items: [ + { + id: 211, + unitPrice: 80, + quantity: 2, + packageLot: { + id: 211, + netWeight: 5 + } + } + ] + }, + { + id: 22, + items: [ + { + id: 221, + unitPrice: 20, + quantity: 2, + packageLot: { + id: 221, + netWeight: 0.5 + } + }, { + id: 222, + unitPrice: 160, + quantity: 1, + packageLot: { + id: 222, + netWeight: 10 + } + } + ] + }, + { + id: 23, + items: [ + { + id: 231, + unitPrice: 160, + quantity: 1, + packageLot: { + id: 231, + netWeight: 10 + } + } + ] + } + ] + } + const sale3 = { + id: 3, + deliveryStart: '2024-01-15T16:00:00', + deliveryStop: '2024-01-15T18:00:00', + deliveryAddressName: 'Place de l\'Etoile', + deliveryAddressLine1: '1 place de l\'Etoile', + deliveryAddressLine2: 'Derrière l\'Arc de Triomphe', + deliveryCity: 'Paris', + deliveryZipCode: '75000', + productions: [ + { + id: 3, + productionType: 'BeefProduction', + slaughterDate: '2024-01-01T10:00:00', + animalLiveWeight: 400, + animalType: 'BEEF_COW', + animalIdentifier: '1234' + } + ], + orders: [ + { + id: 31, + items: [ + { + id: 311, + unitPrice: 160, + quantity: 1, + packageLot: { + id: 311, + netWeight: 10 + } + } + ] + }, + { + id: 32, + items: [ + { + id: 321, + unitPrice: 160, + quantity: 2, + packageLot: { + id: 321, + netWeight: 10 + } + }, + { + id: 322, + unitPrice: 80, + quantity: 1, + packageLot: { + id: 322, + netWeight: 5 + } + } + ] + }, + { + id: 33, + items: [, + { + id: 331, + unitPrice: 80, + quantity: 2, + packageLot: { + id: 331, + netWeight: 5 + } + } + ] + } + ] + } + return [sale1, sale2, sale3] + } +} diff --git a/frontend/app/src/api/mock/MockApiSales.ts b/frontend/app/src/api/mock/MockApiSales.ts index 145384a..c6d0f29 100644 --- a/frontend/app/src/api/mock/MockApiSales.ts +++ b/frontend/app/src/api/mock/MockApiSales.ts @@ -23,44 +23,6 @@ export class MockApiSales { animalType: 'BEEF_HEIFER', animalIdentifier: '9876' } - ], - orders: [ - { - id: 11, - items: [ - { - id: 111, - unitPrice: 20, - quantity: 2, - packageLot: { - id: 111, - netWeight: 0.5 - } - }, { - id: 112, - unitPrice: 160, - quantity: 1, - packageLot: { - id: 112, - netWeight: 10 - } - } - ] - }, - { - id: 12, - items: [ - { - id: 121, - unitPrice: 160, - quantity: 1, - packageLot: { - id: 121, - netWeight: 10 - } - } - ] - } ] } const sale2 = { @@ -81,58 +43,6 @@ export class MockApiSales { animalType: 'BEEF_COW', animalIdentifier: '0987' } - ], - orders: [ - { - id: 21, - items: [ - { - id: 211, - unitPrice: 80, - quantity: 2, - packageLot: { - id: 211, - netWeight: 5 - } - } - ] - }, - { - id: 22, - items: [ - { - id: 221, - unitPrice: 20, - quantity: 2, - packageLot: { - id: 221, - netWeight: 0.5 - } - }, { - id: 222, - unitPrice: 160, - quantity: 1, - packageLot: { - id: 222, - netWeight: 10 - } - } - ] - }, - { - id: 23, - items: [ - { - id: 231, - unitPrice: 160, - quantity: 1, - packageLot: { - id: 231, - netWeight: 10 - } - } - ] - } ] } const sale3 = { @@ -153,59 +63,6 @@ export class MockApiSales { animalType: 'BEEF_COW', animalIdentifier: '1234' } - ], - orders: [ - { - id: 31, - items: [ - { - id: 311, - unitPrice: 160, - quantity: 1, - packageLot: { - id: 311, - netWeight: 10 - } - } - ] - }, - { - id: 32, - items: [ - { - id: 321, - unitPrice: 160, - quantity: 2, - packageLot: { - id: 321, - netWeight: 10 - } - }, - { - id: 322, - unitPrice: 80, - quantity: 1, - packageLot: { - id: 322, - netWeight: 5 - } - } - ] - }, - { - id: 33, - items: [, - { - id: 331, - unitPrice: 80, - quantity: 2, - packageLot: { - id: 331, - netWeight: 5 - } - } - ] - } ] } return [sale1, sale2, sale3] diff --git a/frontend/app/src/authentication/AuthenticationService.ts b/frontend/app/src/authentication/AuthenticationService.ts index 5dc0192..84fa0cd 100644 --- a/frontend/app/src/authentication/AuthenticationService.ts +++ b/frontend/app/src/authentication/AuthenticationService.ts @@ -9,7 +9,7 @@ export class AuthenticationService { } isAuthenticated(): boolean { - return (!!process.env.REACT_APP_MOCK_API) || (!!this.keycloak.authenticated) + return (!!process.env.REACT_APP_MOCK_API && false) || (!!this.keycloak.authenticated) } getCurrentUserName(): string | undefined { @@ -20,4 +20,15 @@ export class AuthenticationService { return this.keycloak.tokenParsed?.name } return undefined - }} \ No newline at end of file + } + + getCurrentUserEmail(): string | undefined { + if(process.env.REACT_APP_MOCK_API) { + return "utilisateur@test.eu" + } + if (this.isAuthenticated()) { + return this.keycloak.tokenParsed?.email + } + return undefined + } +} \ No newline at end of file diff --git a/frontend/app/src/domains/production/components/BeefProductionCustomerCard.tsx b/frontend/app/src/domains/production/components/BeefProductionCustomerCard.tsx index 5521924..509d87f 100644 --- a/frontend/app/src/domains/production/components/BeefProductionCustomerCard.tsx +++ b/frontend/app/src/domains/production/components/BeefProductionCustomerCard.tsx @@ -25,7 +25,7 @@ export default function BeefProductionCustomerCard({production: production}) { return <>
- Abattage bovin + Viande de boeuf
{percentageSold}%
déjà vendu
}>
diff --git a/frontend/app/src/domains/sale/SaleController.tsx b/frontend/app/src/domains/sale/SaleController.tsx index e093fb7..6fb7673 100644 --- a/frontend/app/src/domains/sale/SaleController.tsx +++ b/frontend/app/src/domains/sale/SaleController.tsx @@ -2,14 +2,14 @@ import React from 'react' import { useState } from 'react' import SalesList from './views/SalesList.tsx' -import SaleForm from './views/SaleForm.js' +import SaleForm from './views/SaleForm.tsx' import OrdersList from './views/OrdersList.tsx' import Sale from 'viandeendirect_eu/dist/model/Sale' import Order from 'viandeendirect_eu/dist/model/Order' import OrderView from './views/OrderView.tsx' import OrderForm from './views/OrderForm.tsx' -export default function SaleController() { +export default function SaleController({producer: producer}) { const SALES_LIST_VIEW = 'SALES_LIST_VIEW' const SALE_CREATION_VIEW = 'SALE_CREATION_VIEW' @@ -24,8 +24,8 @@ export default function SaleController() { function getCurrentView() { switch (currentView) { - case SALES_LIST_VIEW: return - case SALE_CREATION_VIEW: return + case SALES_LIST_VIEW: return + case SALE_CREATION_VIEW: return case ORDERS_LIST_VIEW: return createOrder(context)}/> case ORDER_VIEW: return case ORDER_CREATION_VIEW: return diff --git a/frontend/app/src/domains/sale/views/CustomerOrderForm.tsx b/frontend/app/src/domains/sale/views/CustomerOrderForm.tsx index 63c8e05..3231905 100644 --- a/frontend/app/src/domains/sale/views/CustomerOrderForm.tsx +++ b/frontend/app/src/domains/sale/views/CustomerOrderForm.tsx @@ -1,7 +1,7 @@ import React from 'react' import { useState, useEffect } from 'react' -import { Button, Checkbox, Stepper, Step, StepLabel, StepContent, Typography, Box, Toolbar, ButtonGroup } from "@mui/material" +import { Button, Checkbox, Stepper, Step, StepLabel, StepContent, Typography, Box, Toolbar, ButtonGroup, StepButton } from "@mui/material" import dayjs from 'dayjs' import { useKeycloak } from '@react-keycloak/web' @@ -14,12 +14,13 @@ import PackageLot from "viandeendirect_eu/dist/model/PackageLot" import PackageSelector from '../components/PackageSelector.tsx' import { AuthenticationService } from '../../../authentication/AuthenticationService.ts' +import { useCookies } from 'react-cookie' export default function CustomerOrderForm({ sale: sale, returnCallback: returnCallback }) { window.scroll(0,0) const SET_ITEMS_STEP = 1 - const SET_CUSTOMER_STEP = 2 + const AUTHENTICATION_STEP = 2 const CONFIRMATION_STEP = 3 const PAYMENT_STEP = 4 @@ -29,12 +30,26 @@ export default function CustomerOrderForm({ sale: sale, returnCallback: returnCa const [productions, setProductions] = useState>([]) const [order, setOrder] = useState({sale: sale}) + const [completedSteps, setCompletedSteps] = useState>([]) const [items, setItems] = useState>([]) const [activeStep, setActiveStep] = useState(SET_ITEMS_STEP) const [conditionApproved, setConditionApproved] = useState(false) + const [cookies, setCookie, removeCookie] = useCookies(['pendingOrder']); + + useEffect(() => { apiInvoker.callApiAnonymously(api => api.getSaleProductions, sale.id, setProductions) + if (cookies.pendingOrder) { + setItems(cookies.pendingOrder.items) + if(authenticationService.isAuthenticated()) { + setCompletedSteps([SET_ITEMS_STEP, AUTHENTICATION_STEP]) + setActiveStep(CONFIRMATION_STEP) + } else { + setCompletedSteps([SET_ITEMS_STEP]) + setActiveStep(AUTHENTICATION_STEP) + } + } }, []) return @@ -42,8 +57,11 @@ export default function CustomerOrderForm({ sale: sale, returnCallback: returnCa Votre commande pour la vente du {dayjs(sale.deliveryStart).format('DD/MM/YYYY')} - {sale.deliveryAddressName} - - Sélectionnez les articles commandés + + setActiveStep(SET_ITEMS_STEP)}>Sélectionnez les articles commandés
{packageLots()} @@ -53,14 +71,20 @@ export default function CustomerOrderForm({ sale: sale, returnCallback: returnCa
- - {getAuthenticationStepLabel()} + + setActiveStep(AUTHENTICATION_STEP)}>{getAuthenticationStepLabel()} {getAuthenticationStepContent()} - - Acceptez les conditions + + setActiveStep(CONFIRMATION_STEP)}>Acceptez les conditions
@@ -78,10 +102,10 @@ export default function CustomerOrderForm({ sale: sale, returnCallback: returnCa
- J'accepte les conditions + J'accepte les conditions
- +
@@ -108,8 +132,11 @@ export default function CustomerOrderForm({ sale: sale, returnCallback: returnCa } function validateItems() { - setActiveStep(SET_CUSTOMER_STEP) - setOrder({...order, items: items}) + setCompletedSteps([...completedSteps, SET_ITEMS_STEP]) + setActiveStep(AUTHENTICATION_STEP) + const updatedOrder = {...order, items: items} + setCookie('pendingOrder', updatedOrder, {path: "/", maxAge: 3600}) + setOrder(updatedOrder) } function getAuthenticationStepLabel(): string { @@ -122,17 +149,27 @@ export default function CustomerOrderForm({ sale: sale, returnCallback: returnCa function getAuthenticationStepContent() { if (authenticationService.isAuthenticated()) { return - - + + + + } + return + + - } return } - function validateOrder() { + function validateAuthentication() { + setCompletedSteps([...completedSteps, AUTHENTICATION_STEP]) createOrder(order) setActiveStep(CONFIRMATION_STEP) } + function approveConditions() { + setCompletedSteps([...completedSteps, CONFIRMATION_STEP]) + setActiveStep(PAYMENT_STEP) + } + function createOrder(order: Order) { apiInvoker.callApiAuthenticatedly(api => api.createOrder, order, () => {}, keycloak) } @@ -145,7 +182,24 @@ export default function CustomerOrderForm({ sale: sale, returnCallback: returnCa if (conditionApproved && activeStep === PAYMENT_STEP) { return } - return + return + } + + function cancel() { + removeCookie('pendingOrder') + returnCallback(sale) + } + + function login() { + keycloak.login() + } + + function logout() { + keycloak.logout() + } + + function register() { + keycloak.register() } function payOrder() { diff --git a/frontend/app/src/domains/sale/views/SaleForm.js b/frontend/app/src/domains/sale/views/SaleForm.tsx similarity index 98% rename from frontend/app/src/domains/sale/views/SaleForm.js rename to frontend/app/src/domains/sale/views/SaleForm.tsx index 4848a51..3101e1d 100644 --- a/frontend/app/src/domains/sale/views/SaleForm.js +++ b/frontend/app/src/domains/sale/views/SaleForm.tsx @@ -8,7 +8,7 @@ import { DatePickerElement, TextFieldElement, FormContainer, TimePickerElement } import Production from 'viandeendirect_eu/dist/model/Production' import Sale from 'viandeendirect_eu/dist/model/Sale' -import SaleProductionSelector from '../components/SaleProductionSelector' +import SaleProductionSelector from '../components/SaleProductionSelector.js' import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import 'dayjs/locale/fr'; import dayjs from 'dayjs' @@ -19,7 +19,7 @@ const steps = ['Choisir une production', 'Définir le lieu et l\'heure', 'Choisi * @param {Production} production * @returns */ -export default function SaleForm({returnCallback: returnCallback}) { +export default function SaleForm({producer: producer, returnCallback: returnCallback}) { const SELECT_PRODUCTION_STEP = 'SELECT_PRODUCTION_STEP' const SET_DELIVERY_DATE_STEP = 'SET_DELIVERY_DATE_STEP' @@ -194,7 +194,7 @@ export default function SaleForm({returnCallback: returnCallback}) { function validate() { apiBuilder.getAuthenticatedApi(keycloak).then(api => { apiBuilder.invokeAuthenticatedApi(() => { - api.createSale(sale, (error, data, response) => { + api.createProducerSale(producer.id, sale, (error, data, response) => { if (error) { console.error(error) } else { diff --git a/frontend/app/src/domains/sale/views/SalesList.tsx b/frontend/app/src/domains/sale/views/SalesList.tsx index 60ca6b3..c275456 100644 --- a/frontend/app/src/domains/sale/views/SalesList.tsx +++ b/frontend/app/src/domains/sale/views/SalesList.tsx @@ -7,7 +7,7 @@ import { ApiBuilder } from '../../../api/ApiBuilder.ts' import SaleCard from '../components/SaleCard.tsx' -export default function SalesList({manageSaleOrdersCallback: manageSaleOrdersCallback, createSaleCallback: createSaleCallback}) { +export default function SalesList({producer: producer, manageSaleOrdersCallback: manageSaleOrdersCallback, createSaleCallback: createSaleCallback}) { const { keycloak, initialized } = useKeycloak() const apiBuilder = new ApiBuilder() @@ -21,7 +21,7 @@ export default function SalesList({manageSaleOrdersCallback: manageSaleOrdersCal function loadSales() { apiBuilder.getAuthenticatedApi(keycloak).then(api => { apiBuilder.invokeAuthenticatedApi(() => { - api.getSales((error, data, response) => { + api.getProducerSales(producer.id, (error, data, response) => { if (error) { console.error(error) } else { diff --git a/frontend/app/src/layouts/customer/CustomerLayout.tsx b/frontend/app/src/layouts/customer/CustomerLayout.tsx index 4e45d9a..1b60515 100644 --- a/frontend/app/src/layouts/customer/CustomerLayout.tsx +++ b/frontend/app/src/layouts/customer/CustomerLayout.tsx @@ -1,10 +1,11 @@ import React from 'react' import { AppBar, Box, CssBaseline, Toolbar, Typography } from '@mui/material' -import { useState } from 'react' +import { useEffect, useState } from 'react' import Welcome from '../../domains/welcome/Welcome.tsx' import CustomerOrderForm from '../../domains/sale/views/CustomerOrderForm.tsx' import Sale from 'viandeendirect_eu/dist/model/Sale.js' +import { useCookies } from 'react-cookie' export default function CustomerLayout() { @@ -14,6 +15,15 @@ export default function CustomerLayout() { const [mainContent, setMainContent] = useState(WELCOME) const [context, setContext] = useState(undefined) + const [cookies, setCookie, removeCookie] = useCookies(['pendingOrder']); + + useEffect(() => { + if (cookies.pendingOrder) { + createOrder({id: cookies.pendingOrder.sale.id}) + } + }, []) + + function renderMainContent() { switch (mainContent) { case 'WELCOME' : return createOrder(sale)}> diff --git a/frontend/app/src/layouts/producer/AnonymousLayout.js b/frontend/app/src/layouts/producer/AnonymousLayout.tsx similarity index 100% rename from frontend/app/src/layouts/producer/AnonymousLayout.js rename to frontend/app/src/layouts/producer/AnonymousLayout.tsx diff --git a/frontend/app/src/layouts/producer/AuthenticatedLayout.js b/frontend/app/src/layouts/producer/AuthenticatedLayout.tsx similarity index 80% rename from frontend/app/src/layouts/producer/AuthenticatedLayout.js rename to frontend/app/src/layouts/producer/AuthenticatedLayout.tsx index 303dcfe..9c7995d 100644 --- a/frontend/app/src/layouts/producer/AuthenticatedLayout.js +++ b/frontend/app/src/layouts/producer/AuthenticatedLayout.tsx @@ -1,22 +1,33 @@ import { useKeycloak } from '@react-keycloak/web' -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import {AppBar, Box, CssBaseline, IconButton, Toolbar, Typography} from '@mui/material' import {Close, Logout, Menu} from '@mui/icons-material' +import { ApiInvoker } from '../../api/ApiInvoker.ts' + import Dashboard from '../../domains/dashboard/Dashboard.js'; import CustomerController from '../../domains/customer/CustomerController.js'; import GrowerAccount from '../../domains/producer/ProducerAccount.js' import ProductionController from '../../domains/production/ProductionController.js' import SaleController from '../../domains/sale/SaleController.tsx' import SideMenu from './SideMenu.js' +import Producer from 'viandeendirect_eu/dist/model/Producer.js'; +import { AuthenticationService } from '../../authentication/AuthenticationService.ts'; function AuthenticatedLayout() { const { keycloak, initialized } = useKeycloak() - const [sideMenuOpen, setSideMenuOpen] = useState(false); + const apiInvoker = new ApiInvoker() + const [sideMenuOpen, setSideMenuOpen] = useState(false) const [mainContent, setMainContent] = useState('DASHBOARD') - + const [producer, setProducer] = useState() + const authenticationService = new AuthenticationService(keycloak) + + useEffect(() => { + apiInvoker.callApiAuthenticatedly(api => api.getProducer, {'email': authenticationService.getCurrentUserEmail()}, setProducer, keycloak) + }, [keycloak]) + const sideMenuWidth = 240; const handleSideMenuToggle = () => { @@ -31,7 +42,7 @@ function AuthenticatedLayout() { function renderMainContent() { switch (mainContent) { case 'DASHBOARD' : return - case 'SALES' : return + case 'SALES' : return case 'PRODUCTIONS' : return case 'CUSTOMERS' : return case 'GROWER_ACCOUNT' : return diff --git a/frontend/app/src/layouts/producer/LayoutWrapper.js b/frontend/app/src/layouts/producer/LayoutWrapper.js index b967246..d111292 100644 --- a/frontend/app/src/layouts/producer/LayoutWrapper.js +++ b/frontend/app/src/layouts/producer/LayoutWrapper.js @@ -1,7 +1,7 @@ import { useKeycloak } from '@react-keycloak/web' -import AuthenticatedLayout from './AuthenticatedLayout' -import AnonymousLayout from './AnonymousLayout'; +import AuthenticatedLayout from './AuthenticatedLayout.tsx' +import AnonymousLayout from './AnonymousLayout.tsx'; export default function ProducerLayoutWrapper() { diff --git a/openapi/openapi.yml b/openapi/openapi.yml index fee1090..cceb4ce 100644 --- a/openapi/openapi.yml +++ b/openapi/openapi.yml @@ -14,11 +14,18 @@ security: - write paths: - /sales: - summary: Path used to manage the list of sales. + /producers/{producerId}/sales: + summary: Path used to manage the list of sales related to a particular producer. description: "The REST endpoint/path used to list and create zero or more `sale`\ \ entities. This path contains a `GET` and `POST` operation to perform the\ \ list and create tasks, respectively." + parameters: + - name: producerId + description: A unique identifier for a `producer`. + schema: + type: integer + in: path + required: true get: security: - oAuth2ForViandeEnDirect: [read] @@ -31,8 +38,8 @@ paths: items: $ref: '#/components/schemas/Sale' description: Successful response - returns an array of `sale` entities. - operationId: getSales - summary: List all sales for the current user + operationId: getProducerSales + summary: List all sales owned by a spacific producer. description: Gets a list of all `sale` entities. post: security: @@ -51,9 +58,26 @@ paths: schema: $ref: '#/components/schemas/Sale' description: Successful response - returns the created `sale`. - operationId: createSale - summary: Create a sale + operationId: createProducerSale + summary: Create a sale owned by a spacific producer. description: Creates a new instance of a `sale`. + /sales: + summary: Path used to manage the list of all public sales. + get: + security: + - oAuth2ForViandeEnDirect: [read] + responses: + "200": + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Sale' + description: a list of sales + operationId: getSales + summary: Get all the public sales + description: Gets the list of `sale` instances, that are published by producers. /sales/{saleId}: summary: Path used to manage a single sale. description: "The REST endpoint/path used to get, update, and delete single instances\ @@ -299,6 +323,27 @@ paths: description: Successful response - returns the `Customer` entity created. operationId: createCustomer summary: Create a new customers + /producers: + summary: Path used to manage producers + get: + parameters: + - in: query + name: email + schema: + type: string + description: "the email of the Producer to load" + security: + - oAuth2ForViandeEnDirect: [read] + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Producer' + description: Successful response - returns a `Producer` entities. + operationId: getProducer + summary: Get a producer identified by its login. + description: Gets a `Producer` entities. /orders: summary: Path used to manage orders. description: "The REST endpoint/path used to list and create zero or more `Order`\