Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/bin/main/application.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@

logging:
level:
org.springframework.security: DEBUG
org.springframework.web: DEBUG


spring:
mvc:
throw-exception-if-no-handler-found: true
Expand Down
28 changes: 19 additions & 9 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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')
}*/
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import java.io.IOException;

import java.util.List;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

Expand All @@ -29,12 +31,26 @@ public JwtAuthenticationFilter(JwtService jwtService, UserDetailsService userDet
this.logoutService = logoutService;
}

private static final List<String> 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,24 +27,33 @@ 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/**",
"/swagger-ui.html",
"/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/**",
Expand All @@ -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)
)
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Map<String, String>> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package fr.parisnanterre.greentrip.backend.controller;

import java.util.List;
import java.util.Map;

public class ChatRequest {
private List<Map<String, String>> messages;

public List<Map<String, String>> getMessages() {
return messages;
}

public void setMessages(List<Map<String, String>> messages) {
this.messages = messages;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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";
});
}
}
Loading