diff --git a/app/bin/main/application.yml b/app/bin/main/application.yml index d2f7c46..b0f054a 100644 --- a/app/bin/main/application.yml +++ b/app/bin/main/application.yml @@ -1,3 +1,10 @@ + +logging: + level: + org.springframework.security: DEBUG + org.springframework.web: DEBUG + + spring: mvc: throw-exception-if-no-handler-found: true diff --git a/app/build.gradle b/app/build.gradle index 5c8d388..acb89b5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,28 +68,38 @@ test { } dependencies { + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' - implementation 'org.springframework.boot:spring-boot-starter-web:3.3.3' - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0' - implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '3.3.5' + + // Spring Boot Starters (version 3.3.5 partout) + implementation 'org.springframework.boot:spring-boot-starter-web:3.3.5' + implementation 'org.springframework.boot:spring-boot-starter-webflux:3.3.5' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa:3.3.5' + implementation 'org.springframework.boot:spring-boot-starter-security:3.3.5' + implementation 'org.springframework.boot:spring-boot-starter:3.3.5' + + // MySQL implementation group: 'mysql', name: 'mysql-connector-java', version: '8.0.33' + + // Lombok compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.30' annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.30' - implementation group: 'org.springframework.boot', name: 'spring-boot-starter', version: '3.3.5' + + // Sécurité Spring Core implementation group: 'org.springframework.security', name: 'spring-security-core', version: '6.3.3' - implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '3.3.3' + // JSON Web Token libraries implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' - // https://mvnrepository.com/artifact/io.github.cdimascio/java-dotenv - implementation group: 'io.github.cdimascio', name: 'java-dotenv', version: '5.2.2' // Use JUnit test framework. testImplementation libs.junit + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2' testImplementation 'org.springframework.boot:spring-boot-starter-test:3.3.3' } + // Apply a specific Java toolchain to ease working on different environments. java { toolchain { @@ -103,11 +113,11 @@ version = "0.0.1" // Version application { mainClass = 'fr.parisnanterre.greentrip.backend.App' } -/* + // Edit the build task to generate the asciidoc pdf too tasks.named('asciidoctor').configure { dependsOn tasks.named('asciidoctorPdf') } tasks.named('build').configure { dependsOn tasks.named('asciidoctor') -}*/ +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/config/JwtAuthenticationFilter.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/config/JwtAuthenticationFilter.java index 75eff50..d589e8e 100644 --- a/app/src/main/java/fr/parisnanterre/greentrip/backend/config/JwtAuthenticationFilter.java +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/config/JwtAuthenticationFilter.java @@ -16,6 +16,8 @@ import java.io.IOException; +import java.util.List; + @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @@ -29,12 +31,26 @@ public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDet this.logoutService = logoutService; } + private static final List PUBLIC_PATHS = List.of( + "/api/v1/chat", + "/api/v1/messages" + ); + @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain ) throws ServletException, IOException { + String path = request.getRequestURI(); + + // Si la requête cible un chemin public, on laisse passer sans authentifier + if (PUBLIC_PATHS.stream().anyMatch(path::startsWith)) { + filterChain.doFilter(request, response); + return; + } + + final String authHeader = request.getHeader("Authorization"); final String jwt; final String userEmail; diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/config/SecurityConfiguration.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/config/SecurityConfiguration.java index 945b5de..ac377a7 100644 --- a/app/src/main/java/fr/parisnanterre/greentrip/backend/config/SecurityConfiguration.java +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/config/SecurityConfiguration.java @@ -1,8 +1,10 @@ package fr.parisnanterre.greentrip.backend.config; import java.util.List; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @@ -25,11 +27,16 @@ public SecurityConfiguration(JwtAuthenticationFilter jwtAuthFilter, Authenticati @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + System.out.println("🔐 SecurityFilterChain initialized"); http .csrf(csrf -> csrf.disable()) - .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .cors(cors -> cors.disable()) .authorizeHttpRequests(auth -> auth - .requestMatchers( + // Autoriser explicitement les requêtes POST vers /api/v1/chat + .requestMatchers(HttpMethod.POST, "/api/v1/chat").permitAll() + + // Autorisations générales pour les autres endpoints publics + .requestMatchers( "/", "/swagger-ui/**", "/v3/api-docs/**", @@ -37,12 +44,16 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/api-docs/swagger-config", "/api-docs", "/api/v1/auth/**", - "/api/v1/messages/**" - ).permitAll() // On permet l'accès à "/" - .requestMatchers( - "/api/v1/support/**" - ).hasAuthority("ADMIN") - .requestMatchers( + "/api/v1/messages/**", + "/api/v1/chat/**", + "/chat" + ).permitAll() + + // Restrictions pour les endpoints ADMIN + .requestMatchers("/api/v1/support/**").hasAuthority("ADMIN") + + // Authentification requise pour ces endpoints + .requestMatchers( "/api/v1/user/me", "/api/v1/auth/logout", "/api/trips/**", @@ -52,6 +63,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .anyRequest().authenticated() ) + // Toute autre requête doit être authentifiée + .anyRequest().authenticated() + ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) @@ -71,7 +85,7 @@ public CorsConfigurationSource corsConfigurationSource() { "https://greentrip.us" )); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); - configuration.setAllowedHeaders(List.of("Authorization", "Content-Type")); + configuration.setAllowedHeaders(List.of("Authorization", "Content-Type", "Accept", "X-Requested-With")); configuration.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/ChatController.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/ChatController.java new file mode 100644 index 0000000..fd09af2 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/ChatController.java @@ -0,0 +1,39 @@ +package fr.parisnanterre.greentrip.backend.controller; + +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/chat") +public class ChatController { + + @PostMapping + public ChatResponse chat(@RequestBody ChatRequest request) { + List> messages = request.getMessages(); + + if (messages == null || messages.isEmpty()) { + return new ChatResponse("❗ Je n’ai pas compris. Essaie de reformuler !"); + } + + String userMessage = messages.get(messages.size() - 1).get("content").toLowerCase(); + String botReply; + + if (userMessage.contains("randonnée") || userMessage.contains("rando")) { + botReply = "🏞️ Voici quelques idées de randonnées en Île-de-France :\n" + + "• Forêt de Fontainebleau 🌳\n" + + "• Parc naturel du Vexin 🗺️\n" + + "• Gorges de Franchard 🥾\n" + + "• Promenade bleue le long de la Seine 🚶‍♀️"; + } else if (userMessage.contains("bonjour") || userMessage.contains("salut")) { + botReply = "👋 Bonjour ! Je suis ton assistant GreenTrip. Comment puis-je t’aider aujourd’hui ?"; + } else if (userMessage.contains("merci")) { + botReply = "🙏 Avec plaisir ! N’hésite pas si tu as d’autres questions."; + } else { + botReply = "🤖 Je suis en mode démo pour le moment. Pose-moi une question sur la randonnée, les trajets ou dis simplement bonjour !"; + } + + return new ChatResponse(botReply); + } +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/ChatRequest.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/ChatRequest.java new file mode 100644 index 0000000..9e81c80 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/ChatRequest.java @@ -0,0 +1,16 @@ +package fr.parisnanterre.greentrip.backend.controller; + +import java.util.List; +import java.util.Map; + +public class ChatRequest { + private List> messages; + + public List> getMessages() { + return messages; + } + + public void setMessages(List> messages) { + this.messages = messages; + } +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/ChatResponse.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/ChatResponse.java new file mode 100644 index 0000000..434f3e7 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/controller/ChatResponse.java @@ -0,0 +1,19 @@ +package fr.parisnanterre.greentrip.backend.controller; + +public class ChatResponse { + private String content; + + public ChatResponse() {} + + public ChatResponse(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/app/src/main/java/fr/parisnanterre/greentrip/backend/service/ChatbotService.java b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/ChatbotService.java new file mode 100644 index 0000000..69eaac0 --- /dev/null +++ b/app/src/main/java/fr/parisnanterre/greentrip/backend/service/ChatbotService.java @@ -0,0 +1,50 @@ +package fr.parisnanterre.greentrip.backend.service; + +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.Map; + +@Service +public class ChatbotService { + + private final WebClient webClient; + + // Clé à déplacer en variable d'environnement en prod + private final String mistralApiKey = "1vbdiKEC1qrhSTXuanGgaw3KGD4KdzBx"; + + + public ChatbotService(WebClient.Builder webClientBuilder) { + this.webClient = webClientBuilder + .baseUrl("https://api.mistral.ai/v1") + .defaultHeader("Authorization", "Bearer " + mistralApiKey) + .defaultHeader("Content-Type", "application/json") + .defaultHeader("Accept", "application/json") + .build(); + } + + public Mono getChatbotResponse(String model, Object messagesPayload) { + return webClient.post() + .uri("/chat/completions") // ✅ chemin relatif car baseUrl déjà défini + .bodyValue(Map.of( + "model", model, + "messages", messagesPayload + )) + .retrieve() + .bodyToMono(Map.class) + .map(responseMap -> { + Object choicesObj = responseMap.get("choices"); + if (choicesObj instanceof java.util.List choicesList && + !choicesList.isEmpty() && + choicesList.get(0) instanceof Map firstChoiceMap && + ((Map) firstChoiceMap).get("message") instanceof Map messageMap + ) { + Map message = (Map) ((Map) firstChoiceMap).get("message"); + Object content = message.get("content"); + return content instanceof String ? (String) content : "Pas de réponse"; + } + return "Pas de réponse"; + }); + } +}