feat(sprint-17 web 2026-04-25): page publique /pages/public/kpi.xhtml — consultation KPI signée
Page anonyme accessible aux autorités externes via URL signée (HMAC-SHA256 backend). DRY strict — pas de DTO miroir, réutilise KpiPublicSnapshot officiel api 1.0.10. PublicKpiRestClient (@RegisterRestClient sans AuthHeaderFactory — endpoint anonyme) - consulter(token) → KpiPublicSnapshot PublicKpiBean @ViewScoped - charger() lit query param token via FacesContext / viewParam, appelle endpoint - Gestion erreurs HTTP : 401 → "Token invalide ou expiré", 404 → "Organisation introuvable" - Helpers couleurScore (>=80 success / >=60 warning / <60 danger), couleurStatut Page /pages/public/kpi.xhtml - Template public-template.xhtml (sans auth, sans menu) - f:viewParam token + f:viewAction publicKpiBean.charger - Card KPI : score coloré central + 8 indicateurs en grid (Compliance Officer, AG, AIRMS, CMU, KYC, formation LBC/FT, UBO, CAC) - Badge transparency footer : "Toute consultation est tracée dans l'audit trail" - Whitelisté via existing /pages/public/* dans application.properties Bump api 1.0.9 → 1.0.10 Tests (8/8) PublicKpiBean - couleurScore × 4 (success/warning/danger/null) - couleurStatut × 6 cas - isCharge avec/sans snapshot
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
package dev.lions.unionflow.client.service;
|
||||
|
||||
import dev.lions.unionflow.server.api.dto.compliance.response.KpiPublicSnapshot;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.QueryParam;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
|
||||
|
||||
/**
|
||||
* Client REST public KPI (Sprint 17 — pas de header auth, token signé en query).
|
||||
*
|
||||
* <p>Note : pas de {@code @RegisterClientHeaders(AuthHeaderFactory.class)} car endpoint anonyme.
|
||||
*/
|
||||
@RegisterRestClient(configKey = "unionflow-api")
|
||||
@Path("/api/public/kpi")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public interface PublicKpiRestClient {
|
||||
|
||||
@GET
|
||||
KpiPublicSnapshot consulter(@QueryParam("token") String token);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package dev.lions.unionflow.client.view;
|
||||
|
||||
import dev.lions.unionflow.client.service.PublicKpiRestClient;
|
||||
import dev.lions.unionflow.server.api.dto.compliance.response.KpiPublicSnapshot;
|
||||
import jakarta.faces.context.FacesContext;
|
||||
import jakarta.faces.view.ViewScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import java.io.Serializable;
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* Bean public consultation KPI (Sprint 17) — pas d'authentification requise.
|
||||
*
|
||||
* <p>Lit le query param {@code token} dès l'init via {@code @ViewScoped} + viewAction,
|
||||
* appelle l'endpoint public, expose le snapshot à la page.
|
||||
*
|
||||
* @since 2026-04-25 (Sprint 17)
|
||||
*/
|
||||
@Named
|
||||
@ViewScoped
|
||||
public class PublicKpiBean implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Logger LOG = Logger.getLogger(PublicKpiBean.class);
|
||||
|
||||
@Inject @RestClient PublicKpiRestClient client;
|
||||
|
||||
private String token;
|
||||
private KpiPublicSnapshot snapshot;
|
||||
private String erreur;
|
||||
|
||||
/** Appelé via {@code <f:viewAction action="#{publicKpiBean.charger}" />}. */
|
||||
public void charger() {
|
||||
erreur = null;
|
||||
snapshot = null;
|
||||
|
||||
if (token == null || token.isBlank()) {
|
||||
// Token vient du query param via <f:viewParam>
|
||||
token = FacesContext.getCurrentInstance()
|
||||
.getExternalContext()
|
||||
.getRequestParameterMap()
|
||||
.get("token");
|
||||
}
|
||||
|
||||
if (token == null || token.isBlank()) {
|
||||
erreur = "Aucun token fourni dans l'URL.";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
snapshot = client.consulter(token);
|
||||
LOG.infof("PublicKpi chargé — score=%d", snapshot == null ? -1 : snapshot.scoreGlobal());
|
||||
} catch (jakarta.ws.rs.WebApplicationException wae) {
|
||||
int status = wae.getResponse().getStatus();
|
||||
if (status == 401) {
|
||||
erreur = "Token invalide ou expiré.";
|
||||
} else if (status == 404) {
|
||||
erreur = "Organisation introuvable.";
|
||||
} else {
|
||||
erreur = "Erreur HTTP " + status;
|
||||
}
|
||||
LOG.warnf("PublicKpi HTTP %d : %s", status, wae.getMessage());
|
||||
} catch (Exception e) {
|
||||
erreur = "Erreur de chargement : " + e.getMessage();
|
||||
LOG.errorf(e, "PublicKpi exception");
|
||||
}
|
||||
}
|
||||
|
||||
/** Couleur sémantique du score (success / warning / danger). */
|
||||
public String getCouleurScore() {
|
||||
if (snapshot == null) return "secondary";
|
||||
int s = snapshot.scoreGlobal();
|
||||
if (s >= 80) return "success";
|
||||
if (s >= 60) return "warning";
|
||||
return "danger";
|
||||
}
|
||||
|
||||
public String getCouleurStatut(String statut) {
|
||||
if (statut == null) return "secondary";
|
||||
return switch (statut) {
|
||||
case "OK" -> "success";
|
||||
case "RETARD" -> "danger";
|
||||
case "EN_ATTENTE" -> "warning";
|
||||
case "OBLIGATOIRE" -> "info";
|
||||
default -> "secondary";
|
||||
};
|
||||
}
|
||||
|
||||
// Getters / setters
|
||||
public String getToken() { return token; }
|
||||
public void setToken(String token) { this.token = token; }
|
||||
public KpiPublicSnapshot getSnapshot() { return snapshot; }
|
||||
public String getErreur() { return erreur; }
|
||||
public boolean isCharge() { return snapshot != null; }
|
||||
}
|
||||
Reference in New Issue
Block a user