diff --git a/backend/app/src/main/java/eu/viandeendirect/domains/production/BeefProductionService.java b/backend/app/src/main/java/eu/viandeendirect/domains/production/BeefProductionService.java index f53a2f4..df70925 100644 --- a/backend/app/src/main/java/eu/viandeendirect/domains/production/BeefProductionService.java +++ b/backend/app/src/main/java/eu/viandeendirect/domains/production/BeefProductionService.java @@ -1,20 +1,17 @@ package eu.viandeendirect.domains.production; import eu.viandeendirect.api.BeefProductionsApiDelegate; -import eu.viandeendirect.common.ApplicationContextVerifyer; +import eu.viandeendirect.domains.sale.SaleRepository; import eu.viandeendirect.domains.user.ProducerRepository; import eu.viandeendirect.model.BeefProduction; import eu.viandeendirect.model.PackageLot; -import eu.viandeendirect.model.Production; import eu.viandeendirect.model.Sale; -import eu.viandeendirect.domains.sale.SaleRepository; import eu.viandeendirect.security.specs.AuthenticationServiceSpecs; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.Resource; -import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -23,7 +20,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; @Service diff --git a/backend/app/src/main/java/eu/viandeendirect/domains/registration/RegistrationsService.java b/backend/app/src/main/java/eu/viandeendirect/domains/registration/RegistrationsService.java new file mode 100644 index 0000000..f58be7e --- /dev/null +++ b/backend/app/src/main/java/eu/viandeendirect/domains/registration/RegistrationsService.java @@ -0,0 +1,79 @@ +package eu.viandeendirect.domains.registration; + +import eu.viandeendirect.api.RegistrationsApiDelegate; +import eu.viandeendirect.common.EmailService; +import eu.viandeendirect.model.Registration; +import jakarta.mail.MessagingException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; + +@Service +public class RegistrationsService implements RegistrationsApiDelegate { + + @Autowired + EmailService emailService; + + @Override + public ResponseEntity processRegistration(Registration registration) { + try { + emailService.sendMail( + "la.viande.en.direct@gmail.com", + "Nouvelle demande d'inscription d'un producteur sur ViandeEnDirect.eu", + String.format(""" + + Nouvelle demande d'inscription d'un producteur : +
+
+
+ Nom :
+ %s
+
+
+
+ Prénom :
+ %s
+
+
+
+ Email :
+ %s
+
+
+
+ Téléphone :
+ %s
+
+
+
+ Nom de l'exploitation agricole :
+ %s
+
+
+
+ N° SIREN de l'exploitation agricole :
+ %s
+
+
+
+ Description de la production :
+ %s
+
+ + """, + registration.getProducer().getUser().getFirstName(), + registration.getProducer().getUser().getLastName(), + registration.getProducer().getUser().getEmail(), + registration.getProducer().getUser().getPhone(), + registration.getProducer().getLegalName(), + registration.getProducer().getLegalIdentificationNumber(), + registration.getProductionDescription() + ) + ); + } catch (MessagingException e) { + throw new RuntimeException(e); + } + return new ResponseEntity<>(HttpStatus.OK); + } +} 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 9ffde20..112f1c9 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 @@ -59,6 +59,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/error").permitAll() .requestMatchers("/payments/stripeAccountEvents", "/payments/stripeConnectEvents").permitAll() .requestMatchers("/labels/**").hasRole(ROLE_PRODUCER) + .requestMatchers("/registrations").permitAll() ); http.oauth2Login(Customizer.withDefaults()); http.logout(logoutCustomizer -> logoutCustomizer.addLogoutHandler(keycloakLogoutHandler).logoutSuccessUrl("/")); diff --git a/backend/app/src/main/resources/application.properties b/backend/app/src/main/resources/application.properties index b659bfa..91272ae 100644 --- a/backend/app/src/main/resources/application.properties +++ b/backend/app/src/main/resources/application.properties @@ -11,12 +11,3 @@ server.error.include-message=always #---------- spring.liquibase.enabled=true spring.liquibase.change-log=classpath:database/changelog/changelog-master.yaml - -# MOLLIE -#------- -mollie.client-id=app_JSUEw3CWPXcEhFDkCiRCXbPs -mollie.client-secret=NWBG6qptFNVVqq2vSWwbMgW3GryNGdPJPHvy8Uqk -mollie.redirect-url=https://customer.sandbox.viandeendirect.eu -mollie.authorization-url=https://my.mollie.com/oauth2/authorize -mollie.access-token-url=https://api.mollie.com/oauth2/tokens -mollie.resource-owner-url=https://api.mollie.com/v2/organizations/me diff --git a/backend/model/src/main/java/eu/viandeendirect/model/Registration.java b/backend/model/src/main/java/eu/viandeendirect/model/Registration.java new file mode 100644 index 0000000..0d32cfa --- /dev/null +++ b/backend/model/src/main/java/eu/viandeendirect/model/Registration.java @@ -0,0 +1,101 @@ +package eu.viandeendirect.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Generated; +import jakarta.validation.Valid; + +import java.util.Objects; + +/** + * the registration of new producer + */ + +@Schema(name = "Registration", description = "the registration of new producer") +@Generated(value = "org.openapitools.codegen.languages.SpringCodegen") +public class Registration { + + @JsonProperty("producer") + private Producer producer; + + @JsonProperty("productionDescription") + private String productionDescription; + + public Registration producer(Producer producer) { + this.producer = producer; + return this; + } + + /** + * Get producer + * @return producer + */ + @Valid + @Schema(name = "producer", required = false) + public Producer getProducer() { + return producer; + } + + public void setProducer(Producer producer) { + this.producer = producer; + } + + public Registration productionDescription(String productionDescription) { + this.productionDescription = productionDescription; + return this; + } + + /** + * Get productionDescription + * @return productionDescription + */ + + @Schema(name = "productionDescription", required = false) + public String getProductionDescription() { + return productionDescription; + } + + public void setProductionDescription(String productionDescription) { + this.productionDescription = productionDescription; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Registration registration = (Registration) o; + return Objects.equals(this.producer, registration.producer) && + Objects.equals(this.productionDescription, registration.productionDescription); + } + + @Override + public int hashCode() { + return Objects.hash(producer, productionDescription); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Registration {\n"); + sb.append(" producer: ").append(toIndentedString(producer)).append("\n"); + sb.append(" productionDescription: ").append(toIndentedString(productionDescription)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} + diff --git a/frontend/app/package-lock.json b/frontend/app/package-lock.json index b6101e8..ec7ca52 100644 --- a/frontend/app/package-lock.json +++ b/frontend/app/package-lock.json @@ -26,6 +26,7 @@ "react": "^18.2.0", "react-cookie": "^7.0.2", "react-dom": "^18.2.0", + "react-google-recaptcha-v3": "^1.10.1", "react-hook-form": "^7.46.1", "react-hook-form-mui": "^6.8.0", "react-router-dom": "^6.23.0", @@ -15869,6 +15870,18 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" }, + "node_modules/react-google-recaptcha-v3": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.10.1.tgz", + "integrity": "sha512-K3AYzSE0SasTn+XvV2tq+6YaxM+zQypk9rbCgG4OVUt7Rh4ze9basIKefoBz9sC0CNslJj9N1uwTTgRMJQbQJQ==", + "dependencies": { + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "react": "^16.3 || ^17.0 || ^18.0", + "react-dom": "^17.0 || ^18.0" + } + }, "node_modules/react-hook-form": { "version": "7.50.1", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.50.1.tgz", diff --git a/frontend/app/package.json b/frontend/app/package.json index 7300734..82a6591 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -21,6 +21,7 @@ "react": "^18.2.0", "react-cookie": "^7.0.2", "react-dom": "^18.2.0", + "react-google-recaptcha-v3": "^1.10.1", "react-hook-form": "^7.46.1", "react-hook-form-mui": "^6.8.0", "react-router-dom": "^6.23.0", diff --git a/frontend/app/src/App.js b/frontend/app/src/App.js index d4ab858..1613cfe 100644 --- a/frontend/app/src/App.js +++ b/frontend/app/src/App.js @@ -10,6 +10,8 @@ import { CookiesProvider } from 'react-cookie' import './App.css' import { ThemeFactory } from './layouts/ThemeFactory.ts' +import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; + import ViandeEnDirectRouterProvider from './layouts/ViandeEnDirectRouterProvider.tsx' import SnackbarProvider from './domains/commons/components/SnackbarProvider.tsx'; @@ -32,8 +34,8 @@ function App() { - - + + diff --git a/frontend/app/src/api/mock/MockApi.ts b/frontend/app/src/api/mock/MockApi.ts index 1a297c1..0d79d75 100644 --- a/frontend/app/src/api/mock/MockApi.ts +++ b/frontend/app/src/api/mock/MockApi.ts @@ -3,7 +3,6 @@ import { MockApiCustomers } from "./MockApiCustomers.ts" import { MockApiProductions } from "./MockApiProductions.ts" import { MockApiSales } from "./MockApiSales.ts" import { MockApiProducers } from "./MockApiProducers.ts" -import { getPopoverUtilityClass } from "@mui/material" export class MockApi { @@ -157,4 +156,8 @@ export class MockApi { this.log('getProducerPaymentsSummary', args) return this.mockApiProducers.getProducerPaymentsSummary() } + + processRegistration(args) { + this.log('processRegistration', args) + } } \ No newline at end of file diff --git a/frontend/app/src/layouts/producer/AnonymousLayout.tsx b/frontend/app/src/layouts/producer/AnonymousLayout.tsx index 99fc0ec..7ad35dd 100644 --- a/frontend/app/src/layouts/producer/AnonymousLayout.tsx +++ b/frontend/app/src/layouts/producer/AnonymousLayout.tsx @@ -2,12 +2,13 @@ import { styled } from '@mui/material/styles'; import { AppBar, Box, Typography, CssBaseline, Toolbar, Grid, Paper, Button } from "@mui/material"; import { useKeycloak } from '@react-keycloak/web' import React from 'react'; -import { Navigate } from 'react-router-dom'; +import { Navigate, useNavigate } from 'react-router-dom'; import Footer from '../../domains/commons/components/Footer.tsx'; export default function AnonymousLayout() { const { keycloak, initialized } = useKeycloak() + const navigate = useNavigate() const Item = styled(Paper)(({ theme }) => ({ backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff', @@ -22,7 +23,7 @@ export default function AnonymousLayout() { } function register() { - keycloak.register() + navigate('/registration') } function getAnonymousLayout() { diff --git a/frontend/app/src/layouts/producer/ProducerRouterFactory.tsx b/frontend/app/src/layouts/producer/ProducerRouterFactory.tsx index b63ff12..5e21d54 100644 --- a/frontend/app/src/layouts/producer/ProducerRouterFactory.tsx +++ b/frontend/app/src/layouts/producer/ProducerRouterFactory.tsx @@ -17,6 +17,7 @@ import OrderView, { loadOrderViewData } from "../../domains/sale/views/OrderView import ProducerOrderForm, { loadProducerOrderFormData } from "../../domains/sale/views/ProducerOrderForm.tsx"; import CustomersList, { loadCustomersListData } from "../../domains/customer/views/CustomersList.tsx"; import PublicationBeefProductionToSale, { loadPublicationBeefProductionToSaleData } from "../../domains/production/views/beefProduction/PublicationBeefProductionToSale.tsx"; +import RegistrationForm from "./RegistrationForm.tsx"; export class ProducerRouterFactory { getRouter(keycloak) { @@ -95,6 +96,10 @@ export class ProducerRouterFactory { path: '/authentication', element: }, + { + path: '/registration', + element: + }, { path: '/unauthorized', element: , diff --git a/frontend/app/src/layouts/producer/RegistrationForm.tsx b/frontend/app/src/layouts/producer/RegistrationForm.tsx new file mode 100644 index 0000000..4606fc9 --- /dev/null +++ b/frontend/app/src/layouts/producer/RegistrationForm.tsx @@ -0,0 +1,167 @@ +import { Box, CssBaseline, AppBar, Toolbar, Typography, Button, ButtonGroup, Stack } from "@mui/material"; +import React, { useEffect, useState } from "react"; +import { FormContainer, TextFieldElement } from "react-hook-form-mui"; +import { useNavigate } from "react-router-dom"; +import { GoogleReCaptcha, GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; +import { Registration } from "@viandeendirect/api/dist/models"; +import { ApiBuilder } from "../../api/ApiBuilder.ts"; +import { useSnackbar } from "../../domains/commons/components/SnackbarProvider.tsx"; + +export default function RegistrationForm() { + + const navigate = useNavigate() + const [reCaptchaKey, setReCaptchaKey] = useState(undefined) + const [captchaValidated, setCaptchaValidated] = useState(false) + const showSnackbar = useSnackbar() + + useEffect(() => { + const getReCaptchaKey = async () => { + const reCaptchaConfigResponse = await fetch(window.location.origin + '/config/recaptcha.json') + const reCaptchaConfig = await reCaptchaConfigResponse.json() + setReCaptchaKey(reCaptchaConfig.key) + } + getReCaptchaKey() + }, [reCaptchaKey]) + + async function sendNewProducerContact(producerFormData) { + const registration: Registration = { + producer: { + user: { + email: producerFormData.email, + phone: producerFormData.phone, + firstName: producerFormData.firstName, + lastName: producerFormData.lastName + }, + legalName: producerFormData.farmLegalName, + legalIdentificationNumber: producerFormData.farmLegalIdentifier + }, + productionDescription: producerFormData.productionDescription + } + const apiBuilder = new ApiBuilder() + const api = await apiBuilder.getAnonymousApi() + api.processRegistration({registration: registration}) + showSnackbar('Votre demande est bien prise en compte. Nous vous contacterons très prochainement.') + navigate('/') + } + + if (reCaptchaKey) { + return ( + + + + + + + Viande en direct + + + + + + + + Formulaire de création d'un compte producteur +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + +
+
+
+
+
+
+ ) + } + + + function validateReCaptcha(): void { + console.log('reCapcha validated') + setCaptchaValidated(true); + } +} \ No newline at end of file diff --git a/openapi/openapi.yml b/openapi/openapi.yml index 65f36f6..4f2ba85 100644 --- a/openapi/openapi.yml +++ b/openapi/openapi.yml @@ -216,6 +216,24 @@ paths: operationId: getRandomProducerPublicData summary: Get the public data for a random producer + /registrations: + post: + requestBody: + description: New producer's registration data + content: + application/json: + schema: + $ref: '#/components/schemas/Registration' + required: true + security: + - oAuth2ForViandeEnDirect: [write] + responses: + "201": + description: Successful response. + operationId: processRegistration + summary: Process a new producer registration + + /sales: summary: Path used to manage the list of all public sales. parameters: @@ -1167,4 +1185,12 @@ components: yearlyTotal: type: number format: float - description: "the total amount paid for last year" \ No newline at end of file + description: "the total amount paid for last year" + Registration: + description: the registration of new producer + type: object + properties: + producer: + $ref: '#/components/schemas/Producer' + productionDescription: + type: string