Compare commits
2 Commits
v0.8.4-bet
...
v0.8.5-bet
| Author | SHA1 | Date | |
|---|---|---|---|
| f71bf3fcad | |||
| 0cd99dfb32 |
@@ -96,6 +96,15 @@
|
||||
<artifactId>bcprov-jdk18on</artifactId>
|
||||
<version>1.78.1</version>
|
||||
</dependency>
|
||||
<!-- Google Tink : runtime requis par com.nimbusds.jose.crypto.Ed25519Verifier
|
||||
(depuis Nimbus 9.x, la verification EdDSA delegue a Tink.subtle.Ed25519Verify).
|
||||
Tink n'est PAS une dependance transitive de nimbus-jose-jwt → il faut
|
||||
l'ajouter explicitement, sinon NoClassDefFoundError au premier verify(). -->
|
||||
<dependency>
|
||||
<groupId>com.google.crypto.tink</groupId>
|
||||
<artifactId>tink</artifactId>
|
||||
<version>1.14.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -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.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<String> onIllegalArgument(IllegalArgumentException ex) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
|
||||
}
|
||||
|
||||
private GameSystemService.GameSystemData toData(GameSystemDTO dto) {
|
||||
return new GameSystemService.GameSystemData(
|
||||
dto.getName(),
|
||||
|
||||
@@ -252,22 +252,12 @@
|
||||
</div>
|
||||
<div *ngIf="updateStatus?.anyUnknown && !updateStatus?.updateAvailable" class="alert alert-warn">
|
||||
<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 *ngIf="!updateStatus?.updateAvailable && !updateStatus?.anyUnknown" class="hint">
|
||||
Tout est a jour (verifie le {{ updateStatus?.checkedAt | date:'short' }}).
|
||||
</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">
|
||||
<button type="button" class="btn-primary" (click)="applyUpdate()" [disabled]="updateApplying">
|
||||
<lucide-icon [img]="Download" [size]="16"></lucide-icon>
|
||||
@@ -333,7 +323,7 @@
|
||||
<ng-container *ngIf="licenseStatus && licenseStatus.status !== 'NONE'">
|
||||
<div *ngIf="licenseStatus.status === 'VALID'" class="alert alert-success">
|
||||
<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 *ngIf="licenseStatus.status === 'GRACE'" class="alert alert-warn">
|
||||
<lucide-icon [img]="AlertCircle" [size]="16"></lucide-icon>
|
||||
@@ -354,7 +344,7 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<strong>Validite :</strong>
|
||||
jusqu'au {{ formatDate(licenseStatus.expiresAt) }}
|
||||
@@ -408,15 +398,11 @@
|
||||
</div>
|
||||
<div *ngIf="betaStatus?.anyUnknown && !betaStatus?.updateAvailable" class="alert alert-warn">
|
||||
<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>
|
||||
<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>
|
||||
</ng-container>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
formatDate(iso: string | null | undefined): string {
|
||||
if (!iso) return '';
|
||||
|
||||
Reference in New Issue
Block a user