From 72f3791616605b26bc0e536aede9b44271773f0d Mon Sep 17 00:00:00 2001 From: Vincent Guillet Date: Tue, 2 Dec 2025 17:35:53 +0100 Subject: [PATCH] Refactor PrestashopClient and PrestashopProxyController to support raw query handling and simplify proxy GET requests --- .../controller/PrestashopProxyController.java | 48 +---- .../api/service/PrestashopClient.java | 201 +++++------------- 2 files changed, 64 insertions(+), 185 deletions(-) diff --git a/api/src/main/java/fr/gameovergne/api/controller/PrestashopProxyController.java b/api/src/main/java/fr/gameovergne/api/controller/PrestashopProxyController.java index 183446c..423ded5 100644 --- a/api/src/main/java/fr/gameovergne/api/controller/PrestashopProxyController.java +++ b/api/src/main/java/fr/gameovergne/api/controller/PrestashopProxyController.java @@ -1,15 +1,9 @@ -// File: src/main/java/fr/gameovergne/api/controller/PrestashopProxyController.java package fr.gameovergne.api.controller; import fr.gameovergne.api.service.PrestashopClient; import jakarta.servlet.http.HttpServletRequest; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.util.AntPathMatcher; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/ps") @@ -21,37 +15,13 @@ public class PrestashopProxyController { this.prestashopClient = prestashopClient; } - @GetMapping("/**") - public ResponseEntity proxyGet(HttpServletRequest request) { - // Ex: fullPath = /api/ps/categories - String fullPath = (String) request.getAttribute( - HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); - String bestMatchPattern = (String) request.getAttribute( - HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); - - String relativePath = new AntPathMatcher() - .extractPathWithinPattern(bestMatchPattern, fullPath); - - String path = relativePath.isEmpty() ? "/" : "/" + relativePath; - - // Query string brute, déjà encodée (display=%5Bid,name,active%5D&output_format=JSON) - String rawQuery = request.getQueryString(); - - var prestaResponse = prestashopClient.getWithRawQuery(path, rawQuery); - - // Deux options : - // 1) Propager les headers d’origine (dont Content-Type) : - /* - return ResponseEntity - .status(prestaResponse.getStatusCode()) - .headers(prestaResponse.getHeaders()) - .body(prestaResponse.getBody()); - */ - - // 2) Forcer JSON (si tu veux être sûr du Content-Type côté front) : - return ResponseEntity - .status(prestaResponse.getStatusCode()) - .contentType(MediaType.APPLICATION_JSON) - .body(prestaResponse.getBody()); + @GetMapping("/{resource}") + public ResponseEntity proxyGet( + @PathVariable String resource, + HttpServletRequest request + ) { + String rawQuery = request.getQueryString(); // ex: "display=%5Bid,name,active%5D&output_format=JSON" + String body = prestashopClient.getWithRawQuery(resource, rawQuery); + return ResponseEntity.ok(body); } } \ No newline at end of file diff --git a/api/src/main/java/fr/gameovergne/api/service/PrestashopClient.java b/api/src/main/java/fr/gameovergne/api/service/PrestashopClient.java index 3f8bd0f..efbb87c 100644 --- a/api/src/main/java/fr/gameovergne/api/service/PrestashopClient.java +++ b/api/src/main/java/fr/gameovergne/api/service/PrestashopClient.java @@ -1,179 +1,88 @@ -// File: src/main/java/fr/gameovergne/api/service/PrestashopClient.java package fr.gameovergne.api.service; -import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.*; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; -import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; +import java.net.URI; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.Map; @Service -@Slf4j public class PrestashopClient { + private static final Logger log = LoggerFactory.getLogger(PrestashopClient.class); + private final RestClient client; private final String baseUrl; + private final String apiKey; public PrestashopClient( - @Value("${prestashop.base-url}") String baseUrl, - @Value("${prestashop.api-key}") String apiKey + @Value("${prestashop.api.base-url}") String baseUrl, + @Value("${prestashop.api.key}") String apiKey ) { - this.baseUrl = baseUrl; - - String basicAuth = java.util.Base64.getEncoder() - .encodeToString((apiKey + ":").getBytes(StandardCharsets.UTF_8)); - - this.client = RestClient.builder() - .baseUrl(baseUrl) - .defaultHeaders(headers -> { - // Authentification PrestaShop via Basic Auth : key en user, mdp vide - headers.setBasicAuth(apiKey, ""); - - // IMPORTANT : User-Agent “neutre” - headers.set(HttpHeaders.USER_AGENT, "curl/8.10.1"); - - // Accept JSON - headers.setAccept(List.of(MediaType.APPLICATION_JSON, MediaType.ALL)); - }) - .build(); + this.baseUrl = baseUrl; // ex: https://shop.gameovergne.fr/api + this.apiKey = apiKey; log.info("[PrestaShop] Base URL = {}", baseUrl); log.info("[PrestaShop] API key length = {}", apiKey.length()); + + this.client = RestClient.builder() + .defaultHeaders(headers -> { + // PLUS de Basic Auth => on force l’auth par ws_key + headers.set(HttpHeaders.USER_AGENT, "curl/8.10.1"); + headers.setAccept(List.of(MediaType.APPLICATION_JSON, MediaType.ALL)); + }) + .build(); } /** - * Construit l’URL complète en encodant proprement les query params. - * Utilisé par les services "admin" (listSimple, listProducts, etc.). + * Appel "proxy" brut : on reçoit la query string telle quelle et on injecte ws_key proprement. */ - private String buildUri(String path, MultiValueMap params) { - StringBuilder sb = new StringBuilder(); - sb.append(baseUrl); - - if (path != null && !path.isEmpty()) { - if (path.charAt(0) == '/') sb.append(path); - else sb.append('/').append(path); - } - - if (params != null && !params.isEmpty()) { - boolean first = true; - for (Map.Entry> entry : params.entrySet()) { - String key = entry.getKey(); - List values = entry.getValue(); - if (values == null || values.isEmpty()) continue; - - for (String rawValue : values) { - if (first) { - sb.append('?'); - first = false; - } else sb.append('&'); - - // encode la clé - sb.append(URLEncoder.encode(key, StandardCharsets.UTF_8)); - sb.append('='); - - // on encode seulement les parties sensibles, PAS les crochets ni virgules - String safeValue = rawValue - .replace("[", "%5B") - .replace("]", "%5D") - .replace(" ", "") - .replace(",", ","); // laisse la virgule brute - - sb.append(safeValue); - } + public String getWithRawQuery(String resource, String rawQuery) { + try { + // resource = "categories", "products", ... + StringBuilder fullUrl = new StringBuilder(); + fullUrl.append(baseUrl); + if (!baseUrl.endsWith("/")) { + fullUrl.append("/"); } - } + fullUrl.append(resource); - String finalUri = sb.toString(); - log.info("[PrestaShop] built URI = {}", finalUri); - return finalUri; - } + // On construit la query à la main, sans que Spring y touche + String encodedKey = URLEncoder.encode(apiKey, StandardCharsets.UTF_8); + fullUrl.append("?ws_key=").append(encodedKey); - public String getJson(String path, MultiValueMap params) { - String uri = buildUri(path, params); - log.info("[PrestaShop] GET JSON {}", uri); - return client.get() - .uri(uri) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .body(String.class); - } - - public String getXml(String path, MultiValueMap params) { - String uri = buildUri(path, params); - log.info("[PrestaShop] GET XML {}", uri); - return client.get() - .uri(uri) - .accept(MediaType.APPLICATION_XML) - .retrieve() - .body(String.class); - } - - public String postXml(String path, MultiValueMap params, String xmlBody) { - String uri = buildUri(path, params); - log.info("[PrestaShop] POST XML {}", uri); - return client.post() - .uri(uri) - .contentType(MediaType.APPLICATION_XML) - .body(xmlBody) - .retrieve() - .body(String.class); - } - - public String putXml(String path, MultiValueMap params, String xmlBody) { - String uri = buildUri(path, params); - log.info("[PrestaShop] PUT XML {}", uri); - return client.put() - .uri(uri) - .contentType(MediaType.APPLICATION_XML) - .body(xmlBody) - .retrieve() - .body(String.class); - } - - public void delete(String path, MultiValueMap params) { - String uri = buildUri(path, params); - log.info("[PrestaShop] DELETE {}", uri); - client.delete() - .uri(uri) - .retrieve() - .toBodilessEntity(); - } - - /** - * Méthode spéciale pour le proxy brut : - * - on reçoit la query string déjà encodée (rawQuery : "display=%5Bid,name,active%5D&output_format=JSON") - * - on NE REENCODE PAS cette query - * - on renvoie un ResponseEntity complet (status + headers + body) - */ - public ResponseEntity getWithRawQuery(String path, String rawQuery) { - StringBuilder sb = new StringBuilder(); - sb.append(baseUrl); - - if (path != null && !path.isEmpty()) { - if (path.charAt(0) == '/') { - sb.append(path); - } else { - sb.append('/').append(path); + if (rawQuery != null && !rawQuery.isBlank()) { + // ATTENTION : rawQuery vient de request.getQueryString() donc déjà encodée. + // On l’ajoute telle quelle pour ne pas casser les [%5B ... %5D] + fullUrl.append("&").append(rawQuery); } + + String urlString = fullUrl.toString(); + log.info("[PrestaShop] RAW GET via ws_key = {}", urlString); + + return client.get() + .uri(URI.create(urlString)) // IMPORTANT : on donne l’URI complète => pas de ré-encodage + .retrieve() + .body(String.class); + + } catch (Exception e) { + log.error("[PrestaShop] getWithRawQuery error for resource={} rawQuery={}", resource, rawQuery, e); + throw e; } + } - if (rawQuery != null && !rawQuery.isBlank()) { - sb.append('?').append(rawQuery); // déjà encodé - } - - String uri = sb.toString(); - log.info("[PrestaShop] RAW GET {}", uri); - - return client.get() - .uri(uri) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .toEntity(String.class); + // (Optionnel) pour tes autres méthodes listSimple/listProducts, + // tu peux aussi passer par ws_key plutôt que BasicAuth, même principe : + public String getJson(String resource, String fieldsQuery) { + // ex fieldsQuery = "display=[id,name,active]&output_format=JSON" + String rawQuery = fieldsQuery; // tu peux garder le nom si tu veux + return getWithRawQuery(resource, rawQuery); } } \ No newline at end of file