From f71bf3fcad8a7a64e1b05d2e9f365141416388f9 Mon Sep 17 00:00:00 2001 From: "IETM_FIXE\\ietm6" Date: Tue, 19 May 2026 14:38:38 +0200 Subject: [PATCH] =?UTF-8?q?Ajout=20d'un=20globalExceptionHandler=20pour=20?= =?UTF-8?q?intercepter=20toutes=20les=20erreurs=20possibles=20et=20avoir?= =?UTF-8?q?=20un=20peu=20plus=20de=20d=C3=A9tails.=20Suppression=20du=20d?= =?UTF-8?q?=C3=A9tail=20de=20la=20mise=20=C3=A0=20jour=20de=20chaque=20com?= =?UTF-8?q?posant=20:=20l'utilisateur=20ce=20fiche=20de=20savoir=20composa?= =?UTF-8?q?nt=20x=20/=20y=20=C3=A0=20jour=20car=20on=20fera=20la=20mise=20?= =?UTF-8?q?=C3=A0=20jour=20pour=20tout=20=C3=A0=20chaque=20fois=20(m=C3=AA?= =?UTF-8?q?me=20mont=C3=A9e=20en=20version=20pour=20chaque=20composant=20m?= =?UTF-8?q?=C3=AAme=20si=20composant=20y=20non=20touch=C3=A9=20par=20exemp?= =?UTF-8?q?le...=20c'est=20la=20mont=C3=A9e=20en=20version=20de=20l'appli?= =?UTF-8?q?=20qui=20compte)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/GlobalExceptionHandler.java | 97 +++++++++++++++++++ .../web/controller/GameSystemController.java | 7 -- web/src/app/settings/settings.component.html | 28 ++---- web/src/app/settings/settings.component.scss | 26 ----- web/src/app/settings/settings.component.ts | 20 ++++ 5 files changed, 124 insertions(+), 54 deletions(-) create mode 100644 core/src/main/java/com/loremind/infrastructure/web/GlobalExceptionHandler.java diff --git a/core/src/main/java/com/loremind/infrastructure/web/GlobalExceptionHandler.java b/core/src/main/java/com/loremind/infrastructure/web/GlobalExceptionHandler.java new file mode 100644 index 0000000..77e1db2 --- /dev/null +++ b/core/src/main/java/com/loremind/infrastructure/web/GlobalExceptionHandler.java @@ -0,0 +1,97 @@ +package com.loremind.infrastructure.web; + +import jakarta.persistence.EntityNotFoundException; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Intercepteur global d'exceptions pour TOUS les @RestController. + * + *

Role : + *

+ * + *

Important : ne court-circuite PAS les try/catch locaux des controllers + * (ex: LicenseController.install catche InstallException -> 400 lui-meme). + * Ce handler n'attrape QUE ce qui a echappe au catch local. + */ +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * Violation d'invariant domaine (doublons, valeurs invalides, etc.) -> 400. + * Concentre ici la logique qui etait dupliquee dans GameSystemController. + */ + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgument(IllegalArgumentException ex) { + return ResponseEntity.badRequest().body(Map.of("error", safeMessage(ex))); + } + + /** Entite JPA introuvable -> 404. */ + @ExceptionHandler(EntityNotFoundException.class) + public ResponseEntity> handleNotFound(EntityNotFoundException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", safeMessage(ex))); + } + + /** JSON malforme dans le body de la requete -> 400. */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity> handleUnreadable(HttpMessageNotReadableException ex) { + return ResponseEntity.badRequest().body(Map.of("error", "Malformed request body")); + } + + /** Validation @Valid echouee -> 400 avec liste des erreurs par champ. */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation(MethodArgumentNotValidException ex) { + Map fields = new LinkedHashMap<>(); + ex.getBindingResult().getFieldErrors().forEach(e -> + fields.put(e.getField(), e.getDefaultMessage() != null ? e.getDefaultMessage() : "invalid")); + return ResponseEntity.badRequest().body(Map.of( + "error", "Validation failed", + "fields", fields + )); + } + + /** + * Fallback : tout ce qui n'a pas ete catche au-dessus -> 500, mais avec + * un log ERROR explicite (path + stack trace) et un body JSON debuggable + * cote client. C'est LE filet de securite. + * + * Note : on attrape Throwable (pas Exception) pour aussi capturer les + * Error (NoClassDefFoundError, OutOfMemoryError... — cf. incident Tink). + * On NE swallow PAS — on log AVANT de renvoyer une reponse. + */ + @ExceptionHandler(Throwable.class) + public ResponseEntity> handleUnexpected(HttpServletRequest request, Throwable ex) { + log.error("Unhandled exception on {} {}", request.getMethod(), request.getRequestURI(), ex); + Map body = new LinkedHashMap<>(); + body.put("error", "Internal server error"); + body.put("type", ex.getClass().getSimpleName()); + String msg = safeMessage(ex); + if (!msg.isEmpty()) body.put("message", msg); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body); + } + + /** Evite les NPE quand getMessage() est null sur certaines exceptions. */ + private static String safeMessage(Throwable ex) { + return ex.getMessage() != null ? ex.getMessage() : ""; + } +} diff --git a/core/src/main/java/com/loremind/infrastructure/web/controller/GameSystemController.java b/core/src/main/java/com/loremind/infrastructure/web/controller/GameSystemController.java index adb632b..e4d9ddd 100644 --- a/core/src/main/java/com/loremind/infrastructure/web/controller/GameSystemController.java +++ b/core/src/main/java/com/loremind/infrastructure/web/controller/GameSystemController.java @@ -7,7 +7,6 @@ import com.loremind.infrastructure.web.dto.gamesystemcontext.GameSystemDTO; import com.loremind.infrastructure.web.dto.shared.TemplateFieldDTO; import com.loremind.infrastructure.web.mapper.GameSystemMapper; import com.loremind.infrastructure.web.mapper.TemplateFieldMapper; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -72,12 +71,6 @@ public class GameSystemController { return ResponseEntity.noContent().build(); } - /** Mappe les violations d'invariants domaine (doublons de champs, etc.) en 400. */ - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity onIllegalArgument(IllegalArgumentException ex) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage()); - } - private GameSystemService.GameSystemData toData(GameSystemDTO dto) { return new GameSystemService.GameSystemData( dto.getName(), diff --git a/web/src/app/settings/settings.component.html b/web/src/app/settings/settings.component.html index a87addb..3eba6ca 100644 --- a/web/src/app/settings/settings.component.html +++ b/web/src/app/settings/settings.component.html @@ -252,22 +252,12 @@

- Verification impossible pour certaines images — voir details ci-dessous. + Verification impossible (baseline absente ou registry injoignable).
Tout est a jour (verifie le {{ updateStatus?.checkedAt | date:'short' }}).
- -
- Verification beta impossible pour certaines images. + Verification beta impossible (registry beta injoignable ou baseline absente). +
+
+ Aucune version beta plus recente disponible.
- diff --git a/web/src/app/settings/settings.component.scss b/web/src/app/settings/settings.component.scss index 4fa08ac..409583b 100644 --- a/web/src/app/settings/settings.component.scss +++ b/web/src/app/settings/settings.component.scss @@ -322,32 +322,6 @@ accent-color: #6c63ff; } -.update-images { - list-style: none; - padding: 0; - margin: 0.75rem 0; - display: flex; - flex-direction: column; - gap: 0.4rem; -} -.update-images li { - display: flex; - align-items: center; - gap: 0.6rem; - padding: 0.4rem 0.6rem; - background: rgba(255, 255, 255, 0.03); - border-radius: 4px; - font-size: 0.875rem; -} -.badge-update { - margin-left: auto; - background: #6c63ff; - color: white; - font-size: 0.7rem; - font-weight: 700; - padding: 0.15rem 0.5rem; - border-radius: 3px; -} .badge-ok { margin-left: auto; background: rgba(76, 175, 80, 0.2); diff --git a/web/src/app/settings/settings.component.ts b/web/src/app/settings/settings.component.ts index d12df36..cfcf4ca 100644 --- a/web/src/app/settings/settings.component.ts +++ b/web/src/app/settings/settings.component.ts @@ -237,6 +237,26 @@ export class SettingsComponent implements OnInit { }); } + /** + * Mapping tier_id Patreon → nom lisible. Les IDs viennent du dashboard + * Patreon de LoreMind (Settings -> Tiers). Sans entree dans la map, on + * affiche l'ID brut pour rester debuggable. + * + * Si tu ajoutes un nouveau tier Patreon, complete cette map et redeploie. + * (Pas besoin de toucher au backend — c'est juste un libelle d'UI.) + */ + private static readonly TIER_LABELS: Record = { + '28448887': 'Compagnon', + // '0000000': 'Aventurier', + // '0000000': 'Heros', + }; + + /** Libelle lisible d'un tier Patreon, fallback sur l'ID brut. */ + tierLabel(tierId: string | null | undefined): string { + if (!tierId) return ''; + return SettingsComponent.TIER_LABELS[tierId] ?? tierId; + } + /** Format human-readable des dates renvoyees par le backend. */ formatDate(iso: string | null | undefined): string { if (!iso) return '';