/** * Suivi et renouvellement des jetons d'accès. * * Un jeton de page Facebook n'affiche pas toujours de date d'expiration : celui * qui dérive d'une connexion OAuth est permanent, et c'est sa révocation — pas * son échéance — qui casse la publication. L'UI montre donc l'état réel remonté * par le contrôle serveur, pas seulement un décompte de jours. */ import { useMutation, useQuery } from "@tanstack/react-query"; import { AlertTriangle, Calendar, CheckCircle2, Facebook, RefreshCw, ShieldCheck } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, getErrorMessage, queryClient } from "@/lib/queryClient"; import type { ClientSocialPage } from "@shared/schema"; import { format } from "date-fns"; import { fr } from "date-fns/locale"; type TokenStatus = 'valid' | 'expiring' | 'expired' | 'error'; const STATUS_STYLES: Record = { valid: { label: 'Connexion active', color: 'text-success', bgColor: 'bg-success/10' }, expiring: { label: 'Expire bientôt', color: 'text-warning-foreground dark:text-warning', bgColor: 'bg-warning/15' }, expired: { label: 'Connexion expirée', color: 'text-destructive', bgColor: 'bg-destructive/10' }, error: { label: 'Contrôle en échec', color: 'text-destructive', bgColor: 'bg-destructive/10' }, }; function statusOf(page: ClientSocialPage): TokenStatus { return (page.tokenStatus as TokenStatus | null) ?? 'valid'; } /** * Lance l'autorisation Facebook. La connexion se fait par navigation complète * du navigateur (et non en fetch) puisqu'elle passe par le site de Facebook. */ export function useConnectFacebook() { const { toast } = useToast(); const { data: config } = useQuery<{ configured: boolean }>({ queryKey: ['/api/facebook/config'], }); return () => { if (config && !config.configured) { toast({ title: "Facebook n'est pas configuré", description: "Renseignez l'App ID et l'App Secret de votre application Facebook dans les paramètres.", variant: 'destructive', }); return; } window.location.href = '/api/facebook/connect'; }; } export function ConnectFacebookButton({ className }: { className?: string }) { const handleClick = useConnectFacebook(); return ( ); } /** Relance le contrôle serveur, qui renouvelle le jeton s'il le peut. */ export function RefreshTokenButton({ page }: { page: ClientSocialPage }) { const { toast } = useToast(); const refreshMutation = useMutation({ mutationFn: async () => { const response = await apiRequest('POST', `/api/facebook/pages/${page.id}/refresh`); return (await response.json()) as { status: TokenStatus; renewed: boolean; error?: string }; }, onSuccess: (result) => { queryClient.invalidateQueries({ queryKey: ['/api/pages'] }); toast({ title: result.renewed ? 'Jeton renouvelé' : result.status === 'valid' ? 'Jeton valide' : STATUS_STYLES[result.status].label, description: result.error || (result.renewed ? `Un nouveau jeton a été généré pour ${page.pageName}.` : `Aucun renouvellement nécessaire pour ${page.pageName}.`), variant: result.status === 'valid' ? undefined : 'destructive', }); }, onError: (error: unknown) => { toast({ title: 'Contrôle impossible', description: getErrorMessage(error, 'Facebook est injoignable, réessayez plus tard.'), variant: 'destructive', }); }, }); return ( ); } /** * Bloc d'état du jeton : ce que le dernier contrôle a constaté, l'échéance * quand il y en a une, et l'erreur à corriger le cas échéant. */ export function TokenHealthPanel({ page }: { page: ClientSocialPage }) { const status = statusOf(page); const style = STATUS_STYLES[status]; const isTiktok = page.platform === 'tiktok'; // Pour TikTok, c'est l'autorisation (refresh token) qui porte l'échéance. const expiresAt = isTiktok ? page.refreshTokenExpiresAt : page.tokenExpiresAt; return (
{status === 'valid' ? ( ) : ( )} {style.label}
{expiresAt ? ( {isTiktok ? 'Connexion valable jusqu\'au' : 'Expire le'}{' '} {format(new Date(expiresAt), 'd MMMM yyyy', { locale: fr })} ) : page.autoRenew ? ( Sans expiration ) : ( Échéance inconnue )}
{page.autoRenew && (
Renouvellement automatique actif
)} {page.tokenError && (

{page.tokenError}

)} {page.lastTokenCheck && (

Dernier contrôle : {format(new Date(page.lastTokenCheck), 'd MMM yyyy à HH:mm', { locale: fr })}

)}
); } /** Bandeau global : les pages qui réclament une reconnexion manuelle. */ export function TokenAlertBanner({ pages }: { pages: ClientSocialPage[] }) { const failing = pages.filter((page) => { const status = statusOf(page); return status === 'expired' || status === 'error' || status === 'expiring'; }); if (failing.length === 0) return null; return (

{failing.length === 1 ? 'Une page nécessite votre attention' : `${failing.length} pages nécessitent votre attention`}

{failing.map((page) => (

{page.pageName} — {STATUS_STYLES[statusOf(page)].label.toLowerCase()} {page.tokenError ? ` : ${page.tokenError}` : '.'}

))}
); }