import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; import { AlertTriangle, Check, Copy, KeyRound, Plug, Plus, Trash2 } from "lucide-react"; interface ApiKey { id: number; name: string; keyPrefix: string; createdAt: string | null; lastUsedAt: string | null; revokedAt: string | null; } interface ApiKeysResponse { basePath: string; envKeyCount: number; keys: ApiKey[]; } const QUERY_KEY = ["/api/external-api/keys"]; function formatDateTime(value: string | null): string { if (!value) return "—"; return new Date(value).toLocaleString("fr-FR", { dateStyle: "short", timeStyle: "short" }); } // Message d'erreur lisible depuis « 400: {"error":"..."} » renvoyé par apiRequest function errorMessage(error: unknown): string { const raw = error instanceof Error ? error.message : String(error); const json = raw.slice(raw.indexOf(":") + 1).trim(); try { return JSON.parse(json).error || raw; } catch { return raw; } } function CopyButton({ value, label }: { value: string; label: string }) { const [copied, setCopied] = useState(false); const { toast } = useToast(); const copy = async () => { try { await navigator.clipboard.writeText(value); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { toast({ title: "Copie impossible", description: "Sélectionnez le texte et copiez-le à la main.", variant: "destructive" }); } }; return ( ); } export default function ExternalApiSettings() { const { toast } = useToast(); const queryClient = useQueryClient(); const [newKeyName, setNewKeyName] = useState(""); const [createdKey, setCreatedKey] = useState<{ name: string; key: string } | null>(null); const [keyToRevoke, setKeyToRevoke] = useState(null); const { data, isLoading, error } = useQuery({ queryKey: QUERY_KEY }); const createMutation = useMutation({ mutationFn: (name: string) => apiRequest("/api/external-api/keys", "POST", { name }), onSuccess: (result: { key: string; apiKey: ApiKey }) => { setCreatedKey({ name: result.apiKey.name, key: result.key }); setNewKeyName(""); queryClient.invalidateQueries({ queryKey: QUERY_KEY }); }, onError: (err) => toast({ title: "Création impossible", description: errorMessage(err), variant: "destructive" }), }); const revokeMutation = useMutation({ mutationFn: (id: number) => apiRequest(`/api/external-api/keys/${id}`, "DELETE"), onSuccess: () => { toast({ title: "Clé révoquée", description: "Les outils qui l'utilisaient n'ont plus accès à l'API." }); setKeyToRevoke(null); queryClient.invalidateQueries({ queryKey: QUERY_KEY }); }, onError: (err) => toast({ title: "Révocation impossible", description: errorMessage(err), variant: "destructive" }), }); const apiUrl = `${window.location.origin}${data?.basePath ?? "/api/ext/v1"}`; const activeKeys = data?.keys.filter((k) => !k.revokedAt) ?? []; const revokedKeys = data?.keys.filter((k) => k.revokedAt) ?? []; const apiEnabled = activeKeys.length > 0 || (data?.envKeyCount ?? 0) > 0; const handleCreate = (event: React.FormEvent) => { event.preventDefault(); const name = newKeyName.trim(); if (name) createMutation.mutate(name); }; return (
{/* Présentation et adresse */} API externe de rapprochement Permet à un outil externe (n8n, comptabilité, script…) de lire les fournisseurs et les numéros de BL des livraisons, puis d'écrire le numéro de facture, les montants et l'échéance.
État : {isLoading ? ( Chargement… ) : apiEnabled ? ( Active ) : ( Désactivée : aucune clé active )}

La clé s'envoie dans l'en-tête X-API-Key{" "} (ou Authorization: Bearer …).

Principales routes

  • GET /stores — magasins
  • GET /suppliers — fournisseurs
  • GET /deliveries?supplierId=…&hasInvoice=false — livraisons et n° de BL
  • PATCH /deliveries/:id — n° de facture, montants HT/TTC, échéance

Documentation complète : docs/API-RAPPROCHEMENT.md

{/* Clés */} Clés d'API Créez une clé par outil, pour pouvoir en révoquer une sans couper les autres. La clé n'est affichée qu'une seule fois, à sa création.
setNewKeyName(e.target.value)} placeholder="Ex. : n8n comptabilité" maxLength={100} />
{error ? (

Impossible de charger les clés : {errorMessage(error)}

) : isLoading ? (

Chargement…

) : activeKeys.length === 0 ? (

Aucune clé active.

) : (
    {activeKeys.map((apiKey) => (
  • {apiKey.name}

    {apiKey.keyPrefix}… {" · "}créée le {formatDateTime(apiKey.createdAt)} {" · "} {apiKey.lastUsedAt ? `dernière utilisation le ${formatDateTime(apiKey.lastUsedAt)}` : "jamais utilisée"}

  • ))}
)} {(data?.envKeyCount ?? 0) > 0 && (

{data!.envKeyCount === 1 ? "1 clé est aussi définie" : `${data!.envKeyCount} clés sont aussi définies`} dans la variable d'environnement EXTERNAL_API_KEYS du serveur (non modifiables ici).

)} {revokedKeys.length > 0 && (
Clés révoquées ({revokedKeys.length})
    {revokedKeys.map((apiKey) => (
  • {apiKey.name} · {apiKey.keyPrefix}… · révoquée le {formatDateTime(apiKey.revokedAt)}
  • ))}
)}
{/* Clé créée : affichée une seule fois */} { if (!open) setCreatedKey(null); }}> Clé « {createdKey?.name} » créée Copiez-la maintenant et collez-la dans votre outil : elle ne sera plus jamais affichée.
e.currentTarget.select()} aria-label="Clé d'API" /> {createdKey && }

Cette clé donne accès aux livraisons de tous les magasins. Ne la partagez pas ; en cas de doute, révoquez-la.

{/* Confirmation de révocation */} { if (!open) setKeyToRevoke(null); }}> Révoquer la clé « {keyToRevoke?.name} » ? Les outils qui l'utilisent perdront immédiatement l'accès à l'API. Cette action est définitive.
); }