import { headers } from "next/headers"; import { redirect } from "next/navigation"; import type { CSSProperties } from "react"; import { auth } from "@/auth"; import { can } from "@/lib/rbac"; import { SOCIAL_BRAND, SocialIcon } from "@/components/SocialIcons"; import { expiryStatus, getSocialAccounts, listStoredTargets, redirectUri, siteUrl, SOCIAL_LABELS, SOCIAL_NETWORKS, type SocialTarget, } from "@/lib/social-accounts"; import { disconnectSocial, saveSitePublicUrl, saveSocialApp, selectSocialTarget } from "../actions"; export const dynamic = "force-dynamic"; const HELP: Record< (typeof SOCIAL_NETWORKS)[number], { portal: string; steps: string[]; caution?: string } > = { facebook: { portal: "https://developers.facebook.com/apps", steps: [ "Créez une application de type « Business » et relevez l'identifiant et la clé secrète (Paramètres › Général).", "Ajoutez le produit « Connexion Facebook » puis collez l'URL de redirection ci-dessous dans « URI de redirection OAuth valides ».", "Laissez l'application en mode développement et ajoutez le compte de l'association comme administrateur : publier sur votre propre page ne demande alors aucune revue Meta.", ], }, linkedin: { portal: "https://www.linkedin.com/developers/apps", steps: [ "Créez une application rattachée à la page LinkedIn de l'association et relevez le Client ID et le Client Secret (onglet Auth).", "Collez l'URL de redirection ci-dessous dans « Authorized redirect URLs ».", "Onglet Produits : demandez « Community Management API », indispensable pour publier au nom de la page.", ], caution: "LinkedIn soumet cette demande à une revue (page vérifiée, nom légal, adresse, politique de confidentialité) et ses jetons expirent au bout de 60 jours : il faudra recliquer sur Reconnecter environ tous les deux mois.", }, }; function fmtDate(d: Date) { return new Date(d).toLocaleDateString("fr-FR", { day: "numeric", month: "long", year: "numeric" }); } export default async function ReseauxPage({ searchParams, }: { searchParams: Promise<{ error?: string; connected?: string; choose?: string }>; }) { const session = await auth(); if (!can(session?.user.role, "manageSettings")) redirect("/backend"); const { error, connected, choose } = await searchParams; const accounts = await getSocialAccounts(); const byNetwork = new Map(accounts.map((a) => [a.network, a])); const base = await siteUrl(); // Adresse par laquelle l'administrateur consulte cette page : proposée par // défaut pour lui éviter de la recopier à la main. const requestHeaders = await headers(); const host = requestHeaders.get("x-forwarded-host") ?? requestHeaders.get("host") ?? ""; const protocol = requestHeaders.get("x-forwarded-proto") ?? (host.startsWith("localhost") ? "http" : "https"); const detected = host ? `${protocol}://${host}` : ""; // Calculées ici : le rendu des cartes n'est pas asynchrone. const redirectUris = new Map(); for (const network of SOCIAL_NETWORKS) { redirectUris.set(network, await redirectUri(network)); } // Cibles à proposer quand le compte administre plusieurs pages. const targets = new Map(); let targetError: string | null = null; for (const network of SOCIAL_NETWORKS) { const account = byNetwork.get(network); if (account?.accessToken && !account.targetId) { try { targets.set(network, await listStoredTargets(network)); } catch (caught) { targetError = caught instanceof Error ? caught.message : "Pages illisibles."; } } } return (
{error && {error}} {targetError && {targetError}} {connected && ( Compte {SOCIAL_LABELS[connected as "facebook"] ?? connected} connecté. )} {choose && ( Connexion réussie : choisissez la page à utiliser pour les publications. )}

URL publique du site

Adresse à laquelle vos visiteurs accèdent au site. Elle sert à construire l'adresse de retour des connexions Facebook et LinkedIn, et le lien inséré dans les publications.

{!base && detected && (
Adresse détectée depuis votre navigation : {detected}. Vérifiez-la puis enregistrez.
)} {!base && !detected && (
Renseignez cette adresse avant de connecter un réseau social.
)}
{SOCIAL_NETWORKS.map((network) => { const account = byNetwork.get(network); const help = HELP[network]; const status = expiryStatus(account?.expiresAt); const isConnected = !!account?.accessToken && !!account.targetId; const candidates = targets.get(network) ?? []; return (

{SOCIAL_LABELS[network]}

{isConnected && (
Publie sur {account?.targetName}. {status === "never" && " Ce jeton n'expire pas."} {account?.expiresAt && status !== "never" && ` Jeton valable jusqu'au ${fmtDate(account.expiresAt)}.`} {account?.connectedAt && ` Connecté le ${fmtDate(account.connectedAt)}.`}
Pour changer de page, relancez une connexion.
)} {(status === "soon" || status === "expired") && isConnected && ( {status === "expired" ? `Le jeton ${SOCIAL_LABELS[network]} a expiré : les publications échoueront tant que vous n'aurez pas reconnecté le compte.` : `Le jeton ${SOCIAL_LABELS[network]} expire bientôt. Un clic sur Reconnecter suffit à le renouveler.`} )} {candidates.length > 0 && (
Page à utiliser pour les publications
{candidates.map((target) => (
{target.name}
))}
)}
{isConnected ? "Reconnecter" : "Connecter"} {isConnected && (
)}
Comment obtenir ces identifiants ?
    {help.steps.map((step) => (
  1. {step}
  2. ))}
Portail :{" "} {help.portal}
URL de redirection à déclarer
{base ? redirectUris.get(network) : "— renseignez d'abord l'URL publique du site ci-dessus —"}
{help.caution && (
{help.caution}
)}
); })}
); } function StatusChip({ connected, status }: { connected: boolean; status: string }) { const [label, bg, color] = !connected ? ["Non connecté", "#f1efe7", "#a99c82"] : status === "expired" ? ["Jeton expiré", "#fbe9e6", "#d8472b"] : status === "soon" ? ["Expire bientôt", "#fbeede", "#9a6638"] : ["Connecté", "#e6f4ec", "#1f8a5b"]; return ( {label} ); } function Banner({ tone, children }: { tone: "ok" | "warn" | "error"; children: React.ReactNode }) { const palette = { ok: { bg: "#e6f4ec", border: "#c7e6d5", color: "#1f8a5b" }, warn: { bg: "#fbeede", border: "#ecd8b8", color: "#9a6638" }, error: { bg: "#fbe9e6", border: "#f2d5cf", color: "#a8503c" }, }[tone]; return (
{children}
); } const panel: CSSProperties = { background: "#fff", border: "1px solid #e6dcc6", borderRadius: 16, padding: 22, }; const title: CSSProperties = { fontSize: 20, color: "#26201a", }; const submitButton: CSSProperties = { border: "none", background: "#13324F", color: "#fff", fontWeight: 800, fontSize: 14, padding: "11px 20px", borderRadius: 10, cursor: "pointer", };