mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-22 14:48:04 +02:00
Connexion Facebook / LinkedIn en OAuth depuis le backoffice
Les jetons se posaient à la main dans docker-compose. Un écran Backend ›
Réseaux sociaux permet désormais de coller les identifiants de l'application,
cliquer Connecter, choisir la page, et ne plus y revenir.
- Table `social_accounts` : identifiants, jetons et cible par réseau. Tous les
secrets sont chiffrés en AES-256-GCM (`src/lib/crypto.ts`), clé
`SOCIAL_TOKEN_KEY` avec repli sur `AUTH_SECRET`.
- Routes `api/social/[network]/{connect,callback}` : `state` anti-CSRF en cookie
httpOnly, échange du code, récupération des pages administrées. Aucun jeton ne
transite par une URL — les cibles sont relistées côté serveur au moment de la
sélection.
- Facebook : jeton utilisateur longue durée puis jeton de PAGE, qui n'expire
pas. LinkedIn : jeton 60 jours, rafraîchi automatiquement si l'application a
obtenu les jetons de rafraîchissement programmatiques.
- Faute de quoi le backoffice affiche la date d'expiration, un bandeau sur le
tableau de bord à J-7 et un bouton Reconnecter.
- `social.ts` lit les identifiants via `social-accounts.ts` : base d'abord,
variables d'environnement ensuite. Les installations existantes continuent de
fonctionner sans modification.
- Le secret d'application n'est jamais renvoyé au navigateur : champ vide =
valeur conservée.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
This commit is contained in:
19 files changed
+2599
-82
No files matched your search
+29
-45
@@ -1,51 +1,31 @@
|
||||
/**
|
||||
* Publication des promotions sur les pages Facebook / LinkedIn de l'association.
|
||||
*
|
||||
* Les jetons d'accès sont des SECRETS : ils vivent dans les variables
|
||||
* d'environnement du conteneur, jamais en base ni dans le backoffice. Voir
|
||||
* `.env.example` pour la marche à suivre côté Meta / LinkedIn.
|
||||
* Les identifiants et jetons viennent de `social-accounts.ts` : base de données
|
||||
* en premier (posés depuis Backend › Réseaux sociaux), variables
|
||||
* d'environnement en repli.
|
||||
*
|
||||
* Si un réseau n'est pas configuré, le backoffice masque simplement son bouton :
|
||||
* Si un réseau n'est pas configuré, le backoffice masque simplement sa case :
|
||||
* le reste du site fonctionne normalement.
|
||||
*/
|
||||
|
||||
export type SocialNetwork = "facebook" | "linkedin";
|
||||
import {
|
||||
refreshLinkedInIfNeeded,
|
||||
resolveCredentials,
|
||||
siteUrl,
|
||||
type SocialNetwork,
|
||||
} from "./social-accounts";
|
||||
|
||||
export const SOCIAL_NETWORKS: SocialNetwork[] = ["facebook", "linkedin"];
|
||||
export {
|
||||
configuredNetworks,
|
||||
isNetworkConfigured,
|
||||
SOCIAL_LABELS,
|
||||
SOCIAL_NETWORKS,
|
||||
type SocialNetwork,
|
||||
} from "./social-accounts";
|
||||
|
||||
export const SOCIAL_LABELS: Record<SocialNetwork, string> = {
|
||||
facebook: "Facebook",
|
||||
linkedin: "LinkedIn",
|
||||
};
|
||||
|
||||
const FACEBOOK_GRAPH_VERSION = process.env.FACEBOOK_GRAPH_VERSION ?? "v21.0";
|
||||
const LINKEDIN_VERSION = process.env.LINKEDIN_API_VERSION ?? "202506";
|
||||
|
||||
function env(key: string): string {
|
||||
return (process.env[key] ?? "").trim();
|
||||
}
|
||||
|
||||
export function isNetworkConfigured(network: SocialNetwork): boolean {
|
||||
if (network === "facebook") {
|
||||
return !!env("FACEBOOK_PAGE_ID") && !!env("FACEBOOK_PAGE_ACCESS_TOKEN");
|
||||
}
|
||||
return !!linkedinOrganizationUrn() && !!env("LINKEDIN_ACCESS_TOKEN");
|
||||
}
|
||||
|
||||
export function configuredNetworks(): SocialNetwork[] {
|
||||
return SOCIAL_NETWORKS.filter(isNetworkConfigured);
|
||||
}
|
||||
|
||||
function linkedinOrganizationUrn(): string {
|
||||
const urn = env("LINKEDIN_ORGANIZATION_URN");
|
||||
if (urn) return urn;
|
||||
const id = env("LINKEDIN_ORGANIZATION_ID");
|
||||
return id ? `urn:li:organization:${id}` : "";
|
||||
}
|
||||
|
||||
function siteUrl(): string {
|
||||
return (env("NEXT_PUBLIC_SITE_URL") || env("AUTH_URL")).replace(/\/+$/, "");
|
||||
}
|
||||
const FACEBOOK_GRAPH_VERSION = process.env.FACEBOOK_GRAPH_VERSION?.trim() || "v21.0";
|
||||
const LINKEDIN_VERSION = process.env.LINKEDIN_API_VERSION?.trim() || "202506";
|
||||
|
||||
// ---- Contenu du post ----
|
||||
|
||||
@@ -133,9 +113,9 @@ async function readError(res: Response): Promise<string> {
|
||||
// ---- Facebook (Graph API, page de l'association) ----
|
||||
|
||||
async function publishToFacebook(promo: PromoForSharing, message: string): Promise<PublishResult> {
|
||||
const pageId = env("FACEBOOK_PAGE_ID");
|
||||
const token = env("FACEBOOK_PAGE_ACCESS_TOKEN");
|
||||
if (!pageId || !token) throw new SocialPublishError("Facebook n'est pas configuré.");
|
||||
const credentials = await resolveCredentials("facebook");
|
||||
if (!credentials) throw new SocialPublishError("Facebook n'est pas connecté.");
|
||||
const { targetId: pageId, accessToken: token } = credentials;
|
||||
|
||||
const image = await loadPromoImage(promo.imageUrl);
|
||||
const base = `https://graph.facebook.com/${FACEBOOK_GRAPH_VERSION}/${pageId}`;
|
||||
@@ -218,9 +198,13 @@ async function uploadLinkedInImage(
|
||||
}
|
||||
|
||||
async function publishToLinkedIn(promo: PromoForSharing, message: string): Promise<PublishResult> {
|
||||
const token = env("LINKEDIN_ACCESS_TOKEN");
|
||||
const owner = linkedinOrganizationUrn();
|
||||
if (!token || !owner) throw new SocialPublishError("LinkedIn n'est pas configuré.");
|
||||
// Rattrape un jeton proche de l'expiration quand LinkedIn a accordé les
|
||||
// jetons de rafraîchissement programmatiques à l'application.
|
||||
await refreshLinkedInIfNeeded();
|
||||
|
||||
const credentials = await resolveCredentials("linkedin");
|
||||
if (!credentials) throw new SocialPublishError("LinkedIn n'est pas connecté.");
|
||||
const { accessToken: token, targetId: owner } = credentials;
|
||||
|
||||
const image = await loadPromoImage(promo.imageUrl);
|
||||
const imageUrn = image ? await uploadLinkedInImage(token, owner, image) : null;
|
||||
|
||||
Reference in new issue
Block a user