mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-18 04:36:21 +02:00
Paramètres : clé d'API FF Nancy (en-tête X-API-Key)
L'API api.ffnancy.fr exige désormais une clé (401 « Clé d'API requise »). - Nouveau champ « Clé d'API » dans Paramètres → API FF Nancy, stocké dans data/.db-config.json (ffApiKey), jamais renvoyé au navigateur ; vide = conserver la clé enregistrée. Repli sur la variable FF_API_KEY. - Tous les appels à l'API FF envoient l'en-tête X-API-Key (ff_get, proxy commandes-auto, publicités, /api/diag, test de connexion). - Le test de connexion signale explicitement une clé manquante ou refusée. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KdY28Lq8Q8xX4RZsAvWRQ7
This commit is contained in:
11 files changed
+167
-59
No files matched your search
@@ -15,6 +15,8 @@ services:
|
|||||||
- AUTH_SECRET=my-super-secret-key-that-should-be-changed-in-production
|
- AUTH_SECRET=my-super-secret-key-that-should-be-changed-in-production
|
||||||
- DATABASE_URL=postgres://postgres:MOT_DE_PASSE_ICI@postgresbase:5432/collectflow
|
- DATABASE_URL=postgres://postgres:MOT_DE_PASSE_ICI@postgresbase:5432/collectflow
|
||||||
- FF_API_BASE_URL=https://api.ffnancy.fr
|
- FF_API_BASE_URL=https://api.ffnancy.fr
|
||||||
|
# Clé d'API FF Nancy (en-tête X-API-Key) — ou à saisir dans Paramètres.
|
||||||
|
# - FF_API_KEY=VOTRE_CLE_ICI
|
||||||
# La base, Qlik et l'API FF se règlent aussi depuis l'interface (Paramètres).
|
# La base, Qlik et l'API FF se règlent aussi depuis l'interface (Paramètres).
|
||||||
volumes:
|
volumes:
|
||||||
- collectflow-data:/app/data
|
- collectflow-data:/app/data
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ pub fn reset_ff_api_base() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Diagnostic de l'API FF Nancy.
|
/// Diagnostic de l'API FF Nancy.
|
||||||
pub async fn diagnostiquer_api_ff(base: Option<String>) -> Value {
|
pub async fn diagnostiquer_api_ff(base: Option<String>, cle: Option<String>) -> Value {
|
||||||
serde_json::to_value(crate::ff::diagnostic::diagnostiquer_api_ff(base).await).unwrap_or(Value::Null)
|
serde_json::to_value(crate::ff::diagnostic::diagnostiquer_api_ff(base, cle).await).unwrap_or(Value::Null)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extraction Qlik d'un fournisseur sur la fenêtre de la synchro (`QLIK_SYNC_MONTHS_BACK`),
|
/// Extraction Qlik d'un fournisseur sur la fenêtre de la synchro (`QLIK_SYNC_MONTHS_BACK`),
|
||||||
|
|||||||
@@ -116,6 +116,9 @@ pub async fn get_parametres_affichables(ctx: Ctx) -> AppResult<Value> {
|
|||||||
"motDePasseEnregistre": config.qlik_password.as_deref().map(|p| !p.is_empty()).unwrap_or(false) || qlik_pwd_env,
|
"motDePasseEnregistre": config.qlik_password.as_deref().map(|p| !p.is_empty()).unwrap_or(false) || qlik_pwd_env,
|
||||||
},
|
},
|
||||||
"ffApiBaseUrl": config.ff_api_base_url.clone().unwrap_or_default(),
|
"ffApiBaseUrl": config.ff_api_base_url.clone().unwrap_or_default(),
|
||||||
|
// La clé ne quitte pas le serveur : seulement l'indication qu'il y en a une
|
||||||
|
// (réglage enregistré, puis variable d'environnement `FF_API_KEY`).
|
||||||
|
"ffApiKeyEnregistree": crate::ff::api::resoudre_ff_api_key(config.ff_api_key.clone()).is_some(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,12 +222,14 @@ pub async fn test_qlik_connection(ctx: Ctx) -> AppResult<Value> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enregistre l'URL de l'API FF Nancy. Chaîne vide = revenir au défaut.
|
/// Enregistre l'URL de l'API FF Nancy (chaîne vide = revenir au défaut) et sa
|
||||||
|
/// clé d'API (vide ou absente = conserver celle qui est enregistrée).
|
||||||
pub async fn save_ff_api_settings(ctx: Ctx) -> AppResult<Value> {
|
pub async fn save_ff_api_settings(ctx: Ctx) -> AppResult<Value> {
|
||||||
if let Err(e) = ctx.admin().await {
|
if let Err(e) = ctx.admin().await {
|
||||||
return Ok(refus(e));
|
return Ok(refus(e));
|
||||||
}
|
}
|
||||||
let brut: String = ctx.arg::<Option<String>>(0)?.unwrap_or_default();
|
let brut: String = ctx.arg::<Option<String>>(0)?.unwrap_or_default();
|
||||||
|
let cle: Option<String> = ctx.arg::<Option<String>>(1)?.map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
|
||||||
let cleaned = brut.trim().trim_end_matches('/').to_string();
|
let cleaned = brut.trim().trim_end_matches('/').to_string();
|
||||||
let lower = cleaned.to_ascii_lowercase();
|
let lower = cleaned.to_ascii_lowercase();
|
||||||
if !cleaned.is_empty() && !(lower.starts_with("http://") || lower.starts_with("https://")) {
|
if !cleaned.is_empty() && !(lower.starts_with("http://") || lower.starts_with("https://")) {
|
||||||
@@ -232,6 +237,9 @@ pub async fn save_ff_api_settings(ctx: Ctx) -> AppResult<Value> {
|
|||||||
}
|
}
|
||||||
let mut patch = Map::new();
|
let mut patch = Map::new();
|
||||||
patch.insert("ffApiBaseUrl".into(), if cleaned.is_empty() { Value::Null } else { Value::String(cleaned.clone()) });
|
patch.insert("ffApiBaseUrl".into(), if cleaned.is_empty() { Value::Null } else { Value::String(cleaned.clone()) });
|
||||||
|
if let Some(cle) = cle {
|
||||||
|
patch.insert("ffApiKey".into(), Value::String(cle));
|
||||||
|
}
|
||||||
match write_config(patch) {
|
match write_config(patch) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
// Sans cela, l'ancienne URL resterait servie jusqu'à 30 s après la sauvegarde.
|
// Sans cela, l'ancienne URL resterait servie jusqu'à 30 s après la sauvegarde.
|
||||||
@@ -252,7 +260,8 @@ pub async fn test_ff_api_connection(ctx: Ctx) -> AppResult<Value> {
|
|||||||
return Ok(j!({ "success": false, "url": "", "error": e.message }));
|
return Ok(j!({ "success": false, "url": "", "error": e.message }));
|
||||||
}
|
}
|
||||||
let base: Option<String> = ctx.arg(0)?;
|
let base: Option<String> = ctx.arg(0)?;
|
||||||
Ok(super::qlik_bridge::diagnostiquer_api_ff(base).await)
|
let cle: Option<String> = ctx.arg::<Option<String>>(1)?;
|
||||||
|
Ok(super::qlik_bridge::diagnostiquer_api_ff(base, cle).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ pub struct DbConfig {
|
|||||||
/// API REST FF Nancy (ex. https://api.ffnancy.fr).
|
/// API REST FF Nancy (ex. https://api.ffnancy.fr).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ff_api_base_url: Option<String>,
|
pub ff_api_base_url: Option<String>,
|
||||||
|
/// Clé d'API FF Nancy, envoyée dans l'en-tête `X-API-Key`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub ff_api_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_raw_sync() -> Map<String, Value> {
|
fn read_raw_sync() -> Map<String, Value> {
|
||||||
|
|||||||
+58
-24
@@ -42,9 +42,21 @@ pub fn ff_client() -> &'static reqwest::Client {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `fetch(url, ffFetchInit())`.
|
/// `fetch(url, ffFetchInit())`, clé d'API comprise.
|
||||||
pub async fn ff_get(url: &str) -> reqwest::Result<reqwest::Response> {
|
pub async fn ff_get(url: &str) -> reqwest::Result<reqwest::Response> {
|
||||||
ff_client().get(url).header(reqwest::header::CACHE_CONTROL, "no-store").send().await
|
avec_cle_ff(ff_client().get(url).header(reqwest::header::CACHE_CONTROL, "no-store")).await.send().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// En-tête d'authentification attendu par l'API FF Nancy.
|
||||||
|
pub const FF_API_KEY_HEADER: &str = "X-API-Key";
|
||||||
|
|
||||||
|
/// Ajoute la clé d'API enregistrée (en-tête `X-API-Key`) à une requête vers l'API FF.
|
||||||
|
/// Sans clé configurée, la requête part telle quelle.
|
||||||
|
pub async fn avec_cle_ff(req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
||||||
|
match get_ff_api_key().await {
|
||||||
|
Some(cle) => req.header(FF_API_KEY_HEADER, cle),
|
||||||
|
None => req,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Libellé HTTP d'un statut (équivalent de `res.statusText`).
|
/// Libellé HTTP d'un statut (équivalent de `res.statusText`).
|
||||||
@@ -52,7 +64,43 @@ pub fn status_text(status: reqwest::StatusCode) -> &'static str {
|
|||||||
status.canonical_reason().unwrap_or("")
|
status.canonical_reason().unwrap_or("")
|
||||||
}
|
}
|
||||||
|
|
||||||
static FF_API_BASE_CACHE: LazyLock<Mutex<Option<(String, Instant)>>> = LazyLock::new(|| Mutex::new(None));
|
/// Réglages de connexion à l'API FF résolus : adresse et clé.
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct FfApiReglages {
|
||||||
|
base: String,
|
||||||
|
cle: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static FF_API_CACHE: LazyLock<Mutex<Option<(FfApiReglages, Instant)>>> = LazyLock::new(|| Mutex::new(None));
|
||||||
|
|
||||||
|
/// Chaîne non vide après `trim`, sinon `None`.
|
||||||
|
fn non_vide(s: Option<String>) -> Option<String> {
|
||||||
|
s.map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clé d'API FF : réglage enregistré dans Paramètres → variable d'environnement
|
||||||
|
/// `FF_API_KEY` → aucune.
|
||||||
|
pub fn resoudre_ff_api_key(configured: Option<String>) -> Option<String> {
|
||||||
|
non_vide(configured).or_else(|| non_vide(std::env::var("FF_API_KEY").ok()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_ff_api_reglages() -> FfApiReglages {
|
||||||
|
if let Some((value, at)) = FF_API_CACHE.lock().as_ref() {
|
||||||
|
if at.elapsed() < FF_API_BASE_TTL {
|
||||||
|
return value.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fichier absent ou illisible : on retombe sur l'environnement.
|
||||||
|
let config = tokio::task::spawn_blocking(crate::config::read_config).await.unwrap_or_default();
|
||||||
|
let base = non_vide(config.ff_api_base_url)
|
||||||
|
.or_else(|| non_vide(std::env::var("FF_API_BASE_URL").ok()))
|
||||||
|
.unwrap_or_else(|| FF_API_BASE_DEFAULT.to_string())
|
||||||
|
.trim_end_matches('/')
|
||||||
|
.to_string();
|
||||||
|
let value = FfApiReglages { base, cle: resoudre_ff_api_key(config.ff_api_key) };
|
||||||
|
*FF_API_CACHE.lock() = Some((value.clone(), Instant::now()));
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
/// URL de l'API FF Nancy, résolue **au moment de l'appel**.
|
/// URL de l'API FF Nancy, résolue **au moment de l'appel**.
|
||||||
///
|
///
|
||||||
@@ -63,31 +111,17 @@ static FF_API_BASE_CACHE: LazyLock<Mutex<Option<(String, Instant)>>> = LazyLock:
|
|||||||
///
|
///
|
||||||
/// Le résultat est mémorisé 30 s pour ne pas relire le fichier à chaque requête.
|
/// Le résultat est mémorisé 30 s pour ne pas relire le fichier à chaque requête.
|
||||||
pub async fn get_ff_api_base() -> String {
|
pub async fn get_ff_api_base() -> String {
|
||||||
if let Some((value, at)) = FF_API_BASE_CACHE.lock().as_ref() {
|
get_ff_api_reglages().await.base
|
||||||
if at.elapsed() < FF_API_BASE_TTL {
|
}
|
||||||
return value.clone();
|
|
||||||
}
|
/// Clé d'API FF Nancy (même cache que l'adresse).
|
||||||
}
|
pub async fn get_ff_api_key() -> Option<String> {
|
||||||
// Fichier absent ou illisible : on retombe sur l'environnement.
|
get_ff_api_reglages().await.cle
|
||||||
let configured = tokio::task::spawn_blocking(|| crate::config::read_config().ff_api_base_url)
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
.filter(|s| !s.is_empty());
|
|
||||||
let env = std::env::var("FF_API_BASE_URL").ok().filter(|s| !s.is_empty());
|
|
||||||
let value = configured
|
|
||||||
.or(env)
|
|
||||||
.unwrap_or_else(|| FF_API_BASE_DEFAULT.to_string())
|
|
||||||
.trim_end_matches('/')
|
|
||||||
.to_string();
|
|
||||||
*FF_API_BASE_CACHE.lock() = Some((value.clone(), Instant::now()));
|
|
||||||
value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vide le cache — appelé après un enregistrement dans Paramètres.
|
/// Vide le cache — appelé après un enregistrement dans Paramètres.
|
||||||
pub fn reset_ff_api_base_cache() {
|
pub fn reset_ff_api_base_cache() {
|
||||||
*FF_API_BASE_CACHE.lock() = None;
|
*FF_API_CACHE.lock() = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
//! route `/api/ff-status` (tout utilisateur connecté). Chaque appelant fait son
|
//! route `/api/ff-status` (tout utilisateur connecté). Chaque appelant fait son
|
||||||
//! propre contrôle d'accès.
|
//! propre contrôle d'accès.
|
||||||
|
|
||||||
use super::api::{normalize_sync_status, status_text, FfSyncStatus, FF_API_BASE_DEFAULT};
|
use super::api::{get_ff_api_key, normalize_sync_status, status_text, FfSyncStatus, FF_API_BASE_DEFAULT, FF_API_KEY_HEADER};
|
||||||
use super::js::truthy;
|
use super::js::truthy;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -77,7 +77,13 @@ fn message_complet(e: &dyn std::error::Error) -> String {
|
|||||||
/// L'ancien panneau se contentait d'un « HTTP 503 » opaque : impossible de savoir
|
/// L'ancien panneau se contentait d'un « HTTP 503 » opaque : impossible de savoir
|
||||||
/// si l'hôte était injoignable, l'URL erronée ou le service en panne. On distingue
|
/// si l'hôte était injoignable, l'URL erronée ou le service en panne. On distingue
|
||||||
/// donc ici l'échec réseau (DNS, refus de connexion, délai dépassé) du code HTTP.
|
/// donc ici l'échec réseau (DNS, refus de connexion, délai dépassé) du code HTTP.
|
||||||
pub async fn diagnostiquer_api_ff(ff_api_base_url: Option<String>) -> DiagnosticApiFf {
|
///
|
||||||
|
/// `ff_api_key` : clé saisie à tester ; vide ou absente, on reprend la clé enregistrée.
|
||||||
|
pub async fn diagnostiquer_api_ff(ff_api_base_url: Option<String>, ff_api_key: Option<String>) -> DiagnosticApiFf {
|
||||||
|
let cle = match ff_api_key.map(|s| s.trim().to_string()).filter(|s| !s.is_empty()) {
|
||||||
|
Some(c) => Some(c),
|
||||||
|
None => get_ff_api_key().await,
|
||||||
|
};
|
||||||
let saisie = ff_api_base_url.map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
|
let saisie = ff_api_base_url.map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
|
||||||
let base = match saisie {
|
let base = match saisie {
|
||||||
Some(b) => b,
|
Some(b) => b,
|
||||||
@@ -97,9 +103,24 @@ pub async fn diagnostiquer_api_ff(ff_api_base_url: Option<String>) -> Diagnostic
|
|||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
|
|
||||||
let client = reqwest::Client::builder().timeout(Duration::from_millis(8000)).build().unwrap_or_default();
|
let client = reqwest::Client::builder().timeout(Duration::from_millis(8000)).build().unwrap_or_default();
|
||||||
match client.get(&url).header(reqwest::header::CACHE_CONTROL, "no-store").send().await {
|
let mut req = client.get(&url).header(reqwest::header::CACHE_CONTROL, "no-store");
|
||||||
|
if let Some(cle) = &cle {
|
||||||
|
req = req.header(FF_API_KEY_HEADER, cle);
|
||||||
|
}
|
||||||
|
match req.send().await {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
let ms = started.elapsed().as_millis() as u64;
|
let ms = started.elapsed().as_millis() as u64;
|
||||||
|
// 401/403 : le serveur répond, c'est la clé qui manque ou qui est refusée.
|
||||||
|
if matches!(res.status().as_u16(), 401 | 403) {
|
||||||
|
return DiagnosticApiFf::echec(
|
||||||
|
&url,
|
||||||
|
if cle.is_some() {
|
||||||
|
format!("Clé d'API refusée (HTTP {}) — vérifiez la clé saisie.", res.status().as_u16())
|
||||||
|
} else {
|
||||||
|
format!("Clé d'API requise (HTTP {}) — renseignez la clé d'API.", res.status().as_u16())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
if !res.status().is_success() {
|
if !res.status().is_success() {
|
||||||
let txt = status_text(res.status());
|
let txt = status_text(res.status());
|
||||||
return DiagnosticApiFf::echec(
|
return DiagnosticApiFf::echec(
|
||||||
|
|||||||
@@ -247,8 +247,8 @@ async fn module_ff_sur_base_de_test() {
|
|||||||
montrer("get_mensuel_by_articles", api::get_mensuel_by_articles(&ffarts, d, f, 50).await);
|
montrer("get_mensuel_by_articles", api::get_mensuel_by_articles(&ffarts, d, f, 50).await);
|
||||||
montrer("get_referentiel_by_articles", api::get_referentiel_by_articles(&ffarts, 50).await);
|
montrer("get_referentiel_by_articles", api::get_referentiel_by_articles(&ffarts, 50).await);
|
||||||
montrer("get_commandes_by_fournisseur", api::get_commandes_by_fournisseur("F001").await);
|
montrer("get_commandes_by_fournisseur", api::get_commandes_by_fournisseur("F001").await);
|
||||||
montrer("diagnostiquer_api_ff", diagnostic::diagnostiquer_api_ff(None).await);
|
montrer("diagnostiquer_api_ff", diagnostic::diagnostiquer_api_ff(None, None).await);
|
||||||
montrer("diagnostiquer_api_ff(injoignable)", diagnostic::diagnostiquer_api_ff(Some("http://127.0.0.1:9".into())).await);
|
montrer("diagnostiquer_api_ff(injoignable)", diagnostic::diagnostiquer_api_ff(Some("http://127.0.0.1:9".into()), None).await);
|
||||||
|
|
||||||
// ── cache.rs
|
// ── cache.rs
|
||||||
montrer("get_derniere_reception_cached", cache::get_derniere_reception_cached(&codes(&["F002", "F001"])).await.unwrap());
|
montrer("get_derniere_reception_cached", cache::get_derniere_reception_cached(&codes(&["F002", "F001"])).await.unwrap());
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! commandes fournisseurs et publicités.
|
//! commandes fournisseurs et publicités.
|
||||||
|
|
||||||
use super::actions::lister_cadences;
|
use super::actions::lister_cadences;
|
||||||
use super::api::get_ff_api_base;
|
use super::api::{avec_cle_ff, get_ff_api_base};
|
||||||
use super::cache::get_fournisseurs_cached;
|
use super::cache::get_fournisseurs_cached;
|
||||||
use super::js::{get, js_number, js_number_to_string, message_erreur};
|
use super::js::{get, js_number, js_number_to_string, message_erreur};
|
||||||
use super::pg::{
|
use super::pg::{
|
||||||
@@ -223,12 +223,11 @@ async fn fetch_publicites_api(statut: String, page: String) -> PublicitesResult
|
|||||||
}
|
}
|
||||||
let base = get_ff_api_base().await;
|
let base = get_ff_api_base().await;
|
||||||
let res = async {
|
let res = async {
|
||||||
let res = client_publicites()
|
let req = client_publicites()
|
||||||
.get(format!("{base}/api/publicites"))
|
.get(format!("{base}/api/publicites"))
|
||||||
.query(¶ms)
|
.query(¶ms)
|
||||||
.header(reqwest::header::CACHE_CONTROL, "no-store")
|
.header(reqwest::header::CACHE_CONTROL, "no-store");
|
||||||
.send()
|
let res = avec_cle_ff(req).await.send().await?;
|
||||||
.await?;
|
|
||||||
if !res.status().is_success() {
|
if !res.status().is_success() {
|
||||||
return anyhow::Ok(PublicitesResult {
|
return anyhow::Ok(PublicitesResult {
|
||||||
data: json_value!([]),
|
data: json_value!([]),
|
||||||
@@ -262,11 +261,10 @@ async fn fetch_publicites_api(statut: String, page: String) -> PublicitesResult
|
|||||||
async fn fetch_historique_api() -> HistoriqueResult {
|
async fn fetch_historique_api() -> HistoriqueResult {
|
||||||
let base = get_ff_api_base().await;
|
let base = get_ff_api_base().await;
|
||||||
let res = async {
|
let res = async {
|
||||||
let res = client_publicites()
|
let req = client_publicites()
|
||||||
.get(format!("{base}/api/publicites/historique"))
|
.get(format!("{base}/api/publicites/historique"))
|
||||||
.header(reqwest::header::CACHE_CONTROL, "no-store")
|
.header(reqwest::header::CACHE_CONTROL, "no-store");
|
||||||
.send()
|
let res = avec_cle_ff(req).await.send().await?;
|
||||||
.await?;
|
|
||||||
if !res.status().is_success() {
|
if !res.status().is_success() {
|
||||||
return anyhow::Ok(HistoriqueResult {
|
return anyhow::Ok(HistoriqueResult {
|
||||||
data: json_value!([]),
|
data: json_value!([]),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Routes HTTP du module FF : `/api/ff-status`, `/api/diag`,
|
//! Routes HTTP du module FF : `/api/ff-status`, `/api/diag`,
|
||||||
//! `/api/diag/stock-fournisseur`, `/api/test-db`, `/api/commandes-auto`.
|
//! `/api/diag/stock-fournisseur`, `/api/test-db`, `/api/commandes-auto`.
|
||||||
|
|
||||||
use super::api::{ff_client, get_ff_api_base};
|
use super::api::{avec_cle_ff, ff_client, get_ff_api_base};
|
||||||
use super::diagnostic::diagnostiquer_api_ff;
|
use super::diagnostic::diagnostiquer_api_ff;
|
||||||
use super::js::{encode, js_string, message_erreur};
|
use super::js::{encode, js_string, message_erreur};
|
||||||
use super::pg::{pg_diag_fournisseur_derniere_entree, pg_get_stock_for_codeins};
|
use super::pg::{pg_diag_fournisseur_derniere_entree, pg_get_stock_for_codeins};
|
||||||
@@ -31,7 +31,7 @@ fn string_erreur(e: &dyn std::fmt::Display) -> String {
|
|||||||
/// et *quelle adresse* a été appelée. L'ancienne version renvoyait un 503 sans
|
/// et *quelle adresse* a été appelée. L'ancienne version renvoyait un 503 sans
|
||||||
/// détail, impossible à distinguer d'une simple erreur d'URL.
|
/// détail, impossible à distinguer d'une simple erreur d'URL.
|
||||||
pub async fn ff_status(_user: SessionUser) -> Response {
|
pub async fn ff_status(_user: SessionUser) -> Response {
|
||||||
let res = diagnostiquer_api_ff(None).await;
|
let res = diagnostiquer_api_ff(None, None).await;
|
||||||
if !res.success {
|
if !res.success {
|
||||||
return (
|
return (
|
||||||
StatusCode::SERVICE_UNAVAILABLE,
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
@@ -115,7 +115,7 @@ pub async fn commandes_auto(_user: SessionUser, Query(q): Query<HashMap<String,
|
|||||||
}
|
}
|
||||||
let base = get_ff_api_base().await;
|
let base = get_ff_api_base().await;
|
||||||
let url = format!("{base}/api/commandes-auto");
|
let url = format!("{base}/api/commandes-auto");
|
||||||
let res = ff_client().get(&url).query(¶ms).send().await?;
|
let res = avec_cle_ff(ff_client().get(&url).query(¶ms)).await.send().await?;
|
||||||
if !res.status().is_success() {
|
if !res.status().is_success() {
|
||||||
let status = res.status();
|
let status = res.status();
|
||||||
return anyhow::Ok(
|
return anyhow::Ok(
|
||||||
@@ -142,7 +142,7 @@ pub async fn commandes_auto(_user: SessionUser, Query(q): Query<HashMap<String,
|
|||||||
/// Résumé d'une sonde : statut HTTP et aperçu du contenu.
|
/// Résumé d'une sonde : statut HTTP et aperçu du contenu.
|
||||||
async fn probe(url: String) -> Value {
|
async fn probe(url: String) -> Value {
|
||||||
let res = async {
|
let res = async {
|
||||||
let res = reqwest::Client::new().get(&url).header(reqwest::header::CACHE_CONTROL, "no-store").send().await?;
|
let res = avec_cle_ff(reqwest::Client::new().get(&url).header(reqwest::header::CACHE_CONTROL, "no-store")).await.send().await?;
|
||||||
let status = res.status().as_u16();
|
let status = res.status().as_u16();
|
||||||
let text = res.text().await?;
|
let text = res.text().await?;
|
||||||
anyhow::Ok((status, text))
|
anyhow::Ok((status, text))
|
||||||
@@ -189,7 +189,7 @@ fn premiere_liste(data: &Value) -> Vec<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_json(url: &str) -> anyhow::Result<Value> {
|
async fn get_json(url: &str) -> anyhow::Result<Value> {
|
||||||
let res = reqwest::Client::new().get(url).header(reqwest::header::CACHE_CONTROL, "no-store").send().await?;
|
let res = avec_cle_ff(reqwest::Client::new().get(url).header(reqwest::header::CACHE_CONTROL, "no-store")).await.send().await?;
|
||||||
Ok(res.json::<Value>().await?)
|
Ok(res.json::<Value>().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -292,22 +292,28 @@ function SectionFfApi() {
|
|||||||
const [savedUrl, setSavedUrl] = useState<string | null>(null);
|
const [savedUrl, setSavedUrl] = useState<string | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [testedUrl, setTestedUrl] = useState<string | null>(null);
|
const [testedUrl, setTestedUrl] = useState<string | null>(null);
|
||||||
|
// Toujours vide au chargement : la clé enregistrée ne quitte pas le serveur.
|
||||||
|
const [cle, setCle] = useState("");
|
||||||
|
const [cleEnregistree, setCleEnregistree] = useState(false);
|
||||||
|
const [showCle, setShowCle] = useState(false);
|
||||||
|
|
||||||
// Charge l'URL enregistrée pour la préremplir (vide = valeur par défaut).
|
// Charge l'URL enregistrée pour la préremplir (vide = valeur par défaut)
|
||||||
|
// et l'indication qu'une clé est enregistrée.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSavedConfig()
|
loadSavedConfig()
|
||||||
.then((cfg) => {
|
.then((cfg) => {
|
||||||
setSavedUrl(cfg.ffApiBaseUrl || null);
|
setSavedUrl(cfg.ffApiBaseUrl || null);
|
||||||
if (cfg.ffApiBaseUrl) setUrl(cfg.ffApiBaseUrl);
|
if (cfg.ffApiBaseUrl) setUrl(cfg.ffApiBaseUrl);
|
||||||
|
setCleEnregistree(cfg.ffApiKeyEnregistree);
|
||||||
})
|
})
|
||||||
.catch(() => { /* réglage optionnel ; l'échec de lecture est signalé par la page */ });
|
.catch(() => { /* réglage optionnel ; l'échec de lecture est signalé par la page */ });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const runTest = useCallback((candidate?: string) => {
|
const runTest = useCallback((candidate?: string, cleCandidate?: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setStatus(null);
|
setStatus(null);
|
||||||
testFfApiConnection(candidate)
|
testFfApiConnection(candidate, cleCandidate)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setTestedUrl(res.url);
|
setTestedUrl(res.url);
|
||||||
if (res.success) setStatus(res.status as FfSyncStatus);
|
if (res.success) setStatus(res.status as FfSyncStatus);
|
||||||
@@ -322,13 +328,17 @@ function SectionFfApi() {
|
|||||||
const save = async () => {
|
const save = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const res = await saveFfApiSettings(url);
|
const res = await saveFfApiSettings(url, cle || undefined);
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
toast.erreur(res.error ?? "Enregistrement impossible.");
|
toast.erreur(res.error ?? "Enregistrement impossible.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSavedUrl(url.trim() || null);
|
setSavedUrl(url.trim() || null);
|
||||||
toast.succes(url.trim() ? "Adresse de l'API FF enregistrée." : "L'adresse par défaut de l'API FF sera utilisée.");
|
if (cle) {
|
||||||
|
setCle("");
|
||||||
|
setCleEnregistree(true);
|
||||||
|
}
|
||||||
|
toast.succes(url.trim() ? "Réglages de l'API FF enregistrés." : "Réglages de l'API FF enregistrés (adresse par défaut).");
|
||||||
runTest(url);
|
runTest(url);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.erreur(`Enregistrement impossible : ${messageErreur(e)}`);
|
toast.erreur(`Enregistrement impossible : ${messageErreur(e)}`);
|
||||||
@@ -361,13 +371,36 @@ function SectionFfApi() {
|
|||||||
className="w-full font-mono"
|
className="w-full font-mono"
|
||||||
/>
|
/>
|
||||||
</Champ>
|
</Champ>
|
||||||
|
<Champ
|
||||||
|
id="ff-cle"
|
||||||
|
label="Clé d'API"
|
||||||
|
aide={cleEnregistree ? "Laissez vide pour conserver la clé enregistrée." : "Clé fournie par l'administrateur de l'API FF Nancy, envoyée à chaque appel."}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<Input id="ff-cle" type={showCle ? "text" : "password"} placeholder={placeholderMotDePasse(cleEnregistree)} value={cle}
|
||||||
|
onChange={(e) => setCle(e.target.value)}
|
||||||
|
onKeyDown={(e) => { if (e.key === "Enter") void save(); }}
|
||||||
|
className="w-full pr-10 font-mono" autoComplete="new-password" />
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={() => setShowCle((v) => !v)}
|
||||||
|
aria-label={showCle ? "Masquer la clé" : "Afficher la clé"}
|
||||||
|
title={showCle ? "Masquer la clé" : "Afficher la clé"}
|
||||||
|
className="absolute right-0.5 top-1/2 -translate-y-1/2 text-[var(--text-muted)]"
|
||||||
|
>
|
||||||
|
{showCle ? <EyeOff /> : <Eye />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Champ>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3 border-t border-[var(--border)] pt-4">
|
<div className="flex flex-wrap items-center gap-3 border-t border-[var(--border)] pt-4">
|
||||||
<Button onClick={save} disabled={saving}>
|
<Button onClick={save} disabled={saving}>
|
||||||
{saving ? <Loader2 className="animate-spin" /> : <Save />}
|
{saving ? <Loader2 className="animate-spin" /> : <Save />}
|
||||||
Enregistrer
|
Enregistrer
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" onClick={() => runTest(url)} disabled={loading}>
|
<Button variant="outline" onClick={() => runTest(url, cle || undefined)} disabled={loading}>
|
||||||
{loading ? <Loader2 className="animate-spin" /> : <RefreshCw />}
|
{loading ? <Loader2 className="animate-spin" /> : <RefreshCw />}
|
||||||
Tester la connexion
|
Tester la connexion
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export interface ParametresAffichables {
|
|||||||
} | null;
|
} | null;
|
||||||
qlik: { host: string; user: string; motDePasseEnregistre: boolean };
|
qlik: { host: string; user: string; motDePasseEnregistre: boolean };
|
||||||
ffApiBaseUrl: string;
|
ffApiBaseUrl: string;
|
||||||
|
/** Une clé d'API FF est enregistrée (la clé elle-même ne quitte pas le serveur). */
|
||||||
|
ffApiKeyEnregistree: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultatAction {
|
export interface ResultatAction {
|
||||||
@@ -67,12 +69,18 @@ export async function testQlikConnection(qlikHost: string, qlikUser: string, qli
|
|||||||
return callAction("testQlikConnection", [qlikHost, qlikUser, qlikPassword]);
|
return callAction("testQlikConnection", [qlikHost, qlikUser, qlikPassword]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Enregistre l'URL de l'API FF Nancy. Chaîne vide = revenir au défaut. */
|
/**
|
||||||
export async function saveFfApiSettings(ffApiBaseUrl: string): Promise<ResultatAction> {
|
* Enregistre l'URL de l'API FF Nancy (chaîne vide = revenir au défaut) et sa clé
|
||||||
return callAction("saveFfApiSettings", [ffApiBaseUrl]);
|
* d'API (vide ou absente : celle qui est enregistrée est conservée).
|
||||||
|
*/
|
||||||
|
export async function saveFfApiSettings(ffApiBaseUrl: string, ffApiKey?: string): Promise<ResultatAction> {
|
||||||
|
return callAction("saveFfApiSettings", [ffApiBaseUrl, ffApiKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Teste l'API FF Nancy (diagnostic détaillé : DNS, refus, délai, code HTTP). */
|
/**
|
||||||
export async function testFfApiConnection(ffApiBaseUrl?: string): Promise<DiagnosticApiFf> {
|
* Teste l'API FF Nancy (diagnostic détaillé : DNS, refus, délai, code HTTP, clé).
|
||||||
return callAction("testFfApiConnection", [ffApiBaseUrl]);
|
* Clé vide ou absente : on teste avec celle qui est enregistrée.
|
||||||
|
*/
|
||||||
|
export async function testFfApiConnection(ffApiBaseUrl?: string, ffApiKey?: string): Promise<DiagnosticApiFf> {
|
||||||
|
return callAction("testFfApiConnection", [ffApiBaseUrl, ffApiKey]);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user