Compare commits
1 Commits
v0.8.4
...
v0.8.5-bet
| Author | SHA1 | Date | |
|---|---|---|---|
| f71bf3fcad |
@@ -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.
|
||||||
|
*
|
||||||
|
* <p>Role :
|
||||||
|
* <ul>
|
||||||
|
* <li>Logger systematiquement les exceptions non gerees (avec stack trace + path)
|
||||||
|
* — evite d'avoir a creuser dans les logs Docker apres coup.</li>
|
||||||
|
* <li>Renvoyer un JSON propre au client (`{error, type, ...}`) au lieu du 500 nu
|
||||||
|
* par defaut de Spring — utile pour debug cote frontend (visible directement
|
||||||
|
* dans la DevTools reseau).</li>
|
||||||
|
* <li>Mapper les exceptions courantes vers des status HTTP appropries
|
||||||
|
* (IllegalArgumentException -> 400, EntityNotFoundException -> 404).</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>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<Map<String, String>> handleIllegalArgument(IllegalArgumentException ex) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", safeMessage(ex)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Entite JPA introuvable -> 404. */
|
||||||
|
@ExceptionHandler(EntityNotFoundException.class)
|
||||||
|
public ResponseEntity<Map<String, String>> 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<Map<String, String>> 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<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
|
||||||
|
Map<String, String> 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<Map<String, String>> handleUnexpected(HttpServletRequest request, Throwable ex) {
|
||||||
|
log.error("Unhandled exception on {} {}", request.getMethod(), request.getRequestURI(), ex);
|
||||||
|
Map<String, String> 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() : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.dto.shared.TemplateFieldDTO;
|
||||||
import com.loremind.infrastructure.web.mapper.GameSystemMapper;
|
import com.loremind.infrastructure.web.mapper.GameSystemMapper;
|
||||||
import com.loremind.infrastructure.web.mapper.TemplateFieldMapper;
|
import com.loremind.infrastructure.web.mapper.TemplateFieldMapper;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -72,12 +71,6 @@ public class GameSystemController {
|
|||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mappe les violations d'invariants domaine (doublons de champs, etc.) en 400. */
|
|
||||||
@ExceptionHandler(IllegalArgumentException.class)
|
|
||||||
public ResponseEntity<String> onIllegalArgument(IllegalArgumentException ex) {
|
|
||||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
private GameSystemService.GameSystemData toData(GameSystemDTO dto) {
|
private GameSystemService.GameSystemData toData(GameSystemDTO dto) {
|
||||||
return new GameSystemService.GameSystemData(
|
return new GameSystemService.GameSystemData(
|
||||||
dto.getName(),
|
dto.getName(),
|
||||||
|
|||||||
@@ -252,22 +252,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div *ngIf="updateStatus?.anyUnknown && !updateStatus?.updateAvailable" class="alert alert-warn">
|
<div *ngIf="updateStatus?.anyUnknown && !updateStatus?.updateAvailable" class="alert alert-warn">
|
||||||
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
|
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
|
||||||
<span>Verification impossible pour certaines images — voir details ci-dessous.</span>
|
<span>Verification impossible (baseline absente ou registry injoignable).</span>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="!updateStatus?.updateAvailable && !updateStatus?.anyUnknown" class="hint">
|
<div *ngIf="!updateStatus?.updateAvailable && !updateStatus?.anyUnknown" class="hint">
|
||||||
Tout est a jour (verifie le {{ updateStatus?.checkedAt | date:'short' }}).
|
Tout est a jour (verifie le {{ updateStatus?.checkedAt | date:'short' }}).
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="update-images" *ngIf="updateStatus?.images?.length">
|
|
||||||
<li *ngFor="let img of updateStatus?.images">
|
|
||||||
<strong>{{ img.image }}</strong>
|
|
||||||
<span *ngIf="img.status === 'UPDATE_AVAILABLE'" class="badge-update">MAJ dispo</span>
|
|
||||||
<span *ngIf="img.status === 'UP_TO_DATE'" class="badge-ok">a jour</span>
|
|
||||||
<span *ngIf="img.status === 'UNKNOWN'" class="badge-warn"
|
|
||||||
title="Impossible de comparer (baseline absente ou registry injoignable)">verification impossible</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="form-row" *ngIf="updateStatus?.updateAvailable">
|
<div class="form-row" *ngIf="updateStatus?.updateAvailable">
|
||||||
<button type="button" class="btn-primary" (click)="applyUpdate()" [disabled]="updateApplying">
|
<button type="button" class="btn-primary" (click)="applyUpdate()" [disabled]="updateApplying">
|
||||||
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
|
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
|
||||||
@@ -333,7 +323,7 @@
|
|||||||
<ng-container *ngIf="licenseStatus && licenseStatus.status !== 'NONE'">
|
<ng-container *ngIf="licenseStatus && licenseStatus.status !== 'NONE'">
|
||||||
<div *ngIf="licenseStatus.status === 'VALID'" class="alert alert-success">
|
<div *ngIf="licenseStatus.status === 'VALID'" class="alert alert-success">
|
||||||
<lucide-icon [img]="Check" [size]="16"></lucide-icon>
|
<lucide-icon [img]="Check" [size]="16"></lucide-icon>
|
||||||
<span>Compte Patreon connecte. Tier {{ licenseStatus.tierId }} actif.</span>
|
<span>Compte Patreon connecte. Tier {{ tierLabel(licenseStatus.tierId) }} actif.</span>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="licenseStatus.status === 'GRACE'" class="alert alert-warn">
|
<div *ngIf="licenseStatus.status === 'GRACE'" class="alert alert-warn">
|
||||||
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
||||||
@@ -354,7 +344,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="license-info">
|
<ul class="license-info">
|
||||||
<li *ngIf="licenseStatus.tierId"><strong>Tier :</strong> {{ licenseStatus.tierId }}</li>
|
<li *ngIf="licenseStatus.tierId"><strong>Tier :</strong> {{ tierLabel(licenseStatus.tierId) }}</li>
|
||||||
<li *ngIf="licenseStatus.expiresAt">
|
<li *ngIf="licenseStatus.expiresAt">
|
||||||
<strong>Validite :</strong>
|
<strong>Validite :</strong>
|
||||||
jusqu'au {{ formatDate(licenseStatus.expiresAt) }}
|
jusqu'au {{ formatDate(licenseStatus.expiresAt) }}
|
||||||
@@ -408,15 +398,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div *ngIf="betaStatus?.anyUnknown && !betaStatus?.updateAvailable" class="alert alert-warn">
|
<div *ngIf="betaStatus?.anyUnknown && !betaStatus?.updateAvailable" class="alert alert-warn">
|
||||||
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
||||||
<span>Verification beta impossible pour certaines images.</span>
|
<span>Verification beta impossible (registry beta injoignable ou baseline absente).</span>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!betaStatus?.updateAvailable && !betaStatus?.anyUnknown" class="hint">
|
||||||
|
Aucune version beta plus recente disponible.
|
||||||
</div>
|
</div>
|
||||||
<ul class="update-images" *ngIf="betaStatus?.images?.length">
|
|
||||||
<li *ngFor="let img of betaStatus?.images">
|
|
||||||
<strong>{{ img.image }}</strong>
|
|
||||||
<span *ngIf="img.status === 'UPDATE_AVAILABLE'" class="badge-update">version dispo</span>
|
|
||||||
<span *ngIf="img.status === 'UNKNOWN'" class="badge-warn">verification impossible</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -322,32 +322,6 @@
|
|||||||
accent-color: #6c63ff;
|
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 {
|
.badge-ok {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
background: rgba(76, 175, 80, 0.2);
|
background: rgba(76, 175, 80, 0.2);
|
||||||
|
|||||||
@@ -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<string, string> = {
|
||||||
|
'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. */
|
/** Format human-readable des dates renvoyees par le backend. */
|
||||||
formatDate(iso: string | null | undefined): string {
|
formatDate(iso: string | null | undefined): string {
|
||||||
if (!iso) return '';
|
if (!iso) return '';
|
||||||
|
|||||||
Reference in New Issue
Block a user