mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-20 13:48:08 +02:00
Réseaux : contrôle réel de validité des jetons et outils d'inspection
Un jeton de page Facebook n'a pas de date d'expiration, donc `expiryStatus` renvoyait « never » et aucune alerte ne pouvait se déclencher. Mais il meurt quand même : mot de passe du compte changé, rôle d'administrateur perdu sur la page, autorisation retirée, révocation par Meta. On ne l'apprenait qu'au moment où une publication échouait. - `checkTokenHealth()` interroge la plateforme (`/me` côté Graph, les ACL d'organisation côté LinkedIn) et rend un verdict motivé. - L'écran Réseaux sociaux contrôle à chaque affichage : pastille « Jeton expiré », bandeau rouge et réponse brute de la plateforme quand il est mort, ligne verte de confirmation sinon. - Le verdict est persisté (`social_accounts.last_check_*`) : le tableau de bord alerte via `tokenHealthCached()` (fenêtre de 6 h) sans appeler la plateforme à chaque affichage, et croise ce signal avec la dernière tentative de publication refusée pour motif d'autorisation. - Bloc « Outils pour inspecter un jeton » dans l'aide de chaque réseau : débogueur de jeton, outil de jetons d'accès, explorateur d'API Graph, documentation des jetons de longue durée — en précisant que le bouton Connecter fait déjà l'échange court → longue durée → jeton de page, et qu'il n'y a aucun jeton à copier à la main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
This commit is contained in:
8 files changed
+1618
-16
No files matched your search
@@ -141,6 +141,92 @@ export function expiryStatus(expiresAt: Date | null | undefined): ExpiryStatus {
|
||||
return remainingMs <= EXPIRY_WARNING_DAYS * 86_400_000 ? "soon" : "ok";
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie que le jeton fonctionne encore, en interrogeant la plateforme.
|
||||
*
|
||||
* « N'expire pas » ne veut pas dire « ne meurt jamais » : un jeton de page
|
||||
* Facebook est invalidé si le mot de passe du compte connecté change, si ce
|
||||
* compte perd son rôle d'administrateur de la page, si l'autorisation est
|
||||
* retirée, ou si Meta le révoque. Sans cette vérification, on ne l'apprend
|
||||
* qu'au moment où une publication échoue.
|
||||
*/
|
||||
export type TokenHealth =
|
||||
| { ok: true; detail: string }
|
||||
| { ok: false; reason: string };
|
||||
|
||||
export async function checkTokenHealth(network: SocialNetwork): Promise<TokenHealth | null> {
|
||||
const health = await probeToken(network);
|
||||
if (health) await rememberCheck(network, health);
|
||||
return health;
|
||||
}
|
||||
|
||||
/** Contrôle mis en cache : évite un appel réseau à chaque affichage. */
|
||||
export async function tokenHealthCached(
|
||||
network: SocialNetwork,
|
||||
maxAgeMs = 6 * 3_600_000
|
||||
): Promise<TokenHealth | null> {
|
||||
const account = await getSocialAccount(network);
|
||||
if (!account?.accessToken || !account.targetId) return null;
|
||||
|
||||
const fresh =
|
||||
account.lastCheckAt && Date.now() - new Date(account.lastCheckAt).getTime() < maxAgeMs;
|
||||
if (fresh && account.lastCheckOk !== null) {
|
||||
return account.lastCheckOk
|
||||
? { ok: true, detail: "Vérifié récemment" }
|
||||
: { ok: false, reason: account.lastCheckError ?? "Jeton refusé" };
|
||||
}
|
||||
return checkTokenHealth(network);
|
||||
}
|
||||
|
||||
async function rememberCheck(network: SocialNetwork, health: TokenHealth) {
|
||||
await db
|
||||
.update(socialAccounts)
|
||||
.set({
|
||||
lastCheckAt: new Date(),
|
||||
lastCheckOk: health.ok,
|
||||
lastCheckError: health.ok ? null : health.reason.slice(0, 2000),
|
||||
})
|
||||
.where(eq(socialAccounts.network, network));
|
||||
}
|
||||
|
||||
async function probeToken(network: SocialNetwork): Promise<TokenHealth | null> {
|
||||
const credentials = await resolveCredentials(network);
|
||||
if (!credentials) return null;
|
||||
|
||||
try {
|
||||
if (network === "facebook") {
|
||||
// Un jeton de page répond `/me` avec la page elle-même.
|
||||
const res = await fetch(
|
||||
`https://graph.facebook.com/${FACEBOOK_GRAPH_VERSION}/me?${new URLSearchParams({
|
||||
fields: "id,name",
|
||||
access_token: credentials.accessToken,
|
||||
})}`
|
||||
);
|
||||
if (!res.ok) return { ok: false, reason: await readError(res) };
|
||||
const page = (await res.json()) as { id?: string; name?: string };
|
||||
if (page.id && credentials.targetId && page.id !== credentials.targetId) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: `Le jeton ne pointe plus vers la bonne page (${page.name ?? page.id}).`,
|
||||
};
|
||||
}
|
||||
return { ok: true, detail: page.name ?? "Page accessible" };
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
"https://api.linkedin.com/rest/organizationAcls?q=roleAssignee&role=ADMINISTRATOR&state=APPROVED&count=1",
|
||||
{ headers: linkedinHeaders(credentials.accessToken) }
|
||||
);
|
||||
if (!res.ok) return { ok: false, reason: await readError(res) };
|
||||
return { ok: true, detail: "Accès à la page confirmé" };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: error instanceof Error ? error.message : "Plateforme injoignable",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ---- OAuth ----
|
||||
|
||||
export class SocialAuthError extends Error {}
|
||||
|
||||
Reference in new issue
Block a user