mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-17 04:09:51 +02:00
feat(facebook): renouvellement automatique des jetons de page
Un jeton de page collé à la main meurt au bout de 60 jours — ou du jour au lendemain si le mot de passe change, si le compte perd son rôle d'admin ou si Meta révoque l'autorisation — et on ne l'apprenait qu'à la première publication en échec : le contrôle quotidien existant se contentait de marquer la page « expired ». On reprend la chaîne recommandée par Meta, comme dans PleinR : autorisation OAuth, échange du code contre un jeton utilisateur longue durée (~60 j), puis dérivation des jetons de page — qui n'expirent pas. Le jeton utilisateur est conservé chiffré sur chaque page : c'est lui qui permet au TokenManager de régénérer un jeton révoqué sans intervention humaine, et de se prolonger lui-même avant échéance. - facebook_config : App ID / App Secret de l'application, saisis dans les paramètres (chiffrés en base) ou par variables d'environnement. - /api/facebook : configuration, connexion OAuth, callback qui crée ou met à jour les pages, contrôle d'une page à la demande, contrôle global. - TokenManager : /debug_token pour connaître l'état réel du jeton, puis renouvellement automatique 15 jours avant l'échéance ou dès révocation ; repli sur le contrôle historique tant que l'application n'est pas configurée ou si Facebook est injoignable, pour ne pas passer l'UI au rouge sur une panne réseau. Le cron passe à deux fois par jour. - Les comptes Instagram professionnels rattachés à une page reçoivent le même traitement, puisqu'ils publient avec le jeton de cette page. - L'UI montre l'état constaté (jeton valide, bientôt expiré, révoqué), l'erreur à corriger et un bouton de contrôle immédiat ; un bandeau signale les pages à reconnecter. - /api/pages ne renvoie plus aucun jeton au navigateur, seulement un indicateur de renouvellement automatique. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qns6Gcpd95bCHWUxySh4Ew
This commit is contained in:
13 files changed
+1385
-249
No files matched your search
@@ -38,6 +38,15 @@ export const socialPages = pgTable("social_pages", {
|
||||
tokenStatus: tokenStatusEnum("token_status").default("valid"),
|
||||
lastTokenCheck: timestamp("last_token_check"),
|
||||
|
||||
// Jeton utilisateur longue durée (~60 j) obtenu via OAuth Facebook, chiffré au
|
||||
// repos. C'est lui qui permet de régénérer un token de page sans intervention
|
||||
// humaine : sans lui, un token révoqué impose une reconnexion manuelle.
|
||||
userAccessToken: text("user_access_token"),
|
||||
userTokenExpiresAt: timestamp("user_token_expires_at"),
|
||||
// Dernière erreur rencontrée lors du contrôle du token, affichée dans l'UI
|
||||
// pour que l'utilisateur sache quoi faire (reconnecter, réautoriser…).
|
||||
tokenError: text("token_error"),
|
||||
|
||||
// OAuth TikTok : l'access token n'est valable que 24h, il doit être renouvelé
|
||||
// avec le refresh token (valable 1 an) avant chaque publication.
|
||||
refreshToken: text("refresh_token"),
|
||||
@@ -93,6 +102,7 @@ export const insertSocialPageSchema = createInsertSchema(socialPages).omit({
|
||||
createdAt: true,
|
||||
tokenStatus: true, // Managed by System
|
||||
lastTokenCheck: true, // Managed by System
|
||||
tokenError: true, // Managed by System
|
||||
});
|
||||
|
||||
export const insertPostAnalyticsSchema = createInsertSchema(postAnalytics).omit({
|
||||
@@ -143,6 +153,20 @@ export const openrouterConfig = pgTable("openrouter_config", {
|
||||
|
||||
// Application développeur TikTok (globale) : un seul client_key pour toute
|
||||
// l'instance, chaque magasin autorise ensuite son propre compte via OAuth.
|
||||
/**
|
||||
* Application développeur Facebook (globale à l'instance). Elle rend possible le
|
||||
* renouvellement automatique des tokens de page : sans app id / app secret, on
|
||||
* ne peut ni échanger un code OAuth, ni prolonger un jeton, ni interroger
|
||||
* /debug_token.
|
||||
*/
|
||||
export const facebookConfig = pgTable("facebook_config", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
appId: text("app_id").notNull(),
|
||||
appSecret: text("app_secret").notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
});
|
||||
|
||||
export const tiktokConfig = pgTable("tiktok_config", {
|
||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||
clientKey: text("client_key").notNull(),
|
||||
@@ -432,6 +456,19 @@ export const updateCloudinaryConfigSchema = insertCloudinaryConfigSchema.partial
|
||||
apiSecret: true,
|
||||
});
|
||||
|
||||
export const insertFacebookConfigSchema = createInsertSchema(facebookConfig).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
}).extend({
|
||||
appId: z.string().trim().min(1, "L'App ID ne peut pas être vide"),
|
||||
appSecret: z.string().trim().min(1, "L'App Secret ne peut pas être vide"),
|
||||
});
|
||||
|
||||
export const updateFacebookConfigSchema = insertFacebookConfigSchema.partial({
|
||||
appSecret: true,
|
||||
});
|
||||
|
||||
export const insertTiktokConfigSchema = createInsertSchema(tiktokConfig).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
@@ -504,6 +541,19 @@ export type InsertAudioTrack = z.infer<typeof insertAudioTrackSchema>;
|
||||
|
||||
export type AppConfig = typeof appConfig.$inferSelect;
|
||||
|
||||
/**
|
||||
* Page telle qu'elle est renvoyée au navigateur : les jetons restent au serveur,
|
||||
* `autoRenew` dit simplement si la page sait se renouveler toute seule.
|
||||
*/
|
||||
export type ClientSocialPage = Omit<
|
||||
SocialPage,
|
||||
'accessToken' | 'userAccessToken' | 'refreshToken'
|
||||
> & { autoRenew: boolean };
|
||||
|
||||
export type FacebookConfig = typeof facebookConfig.$inferSelect;
|
||||
export type InsertFacebookConfig = z.infer<typeof insertFacebookConfigSchema>;
|
||||
export type UpdateFacebookConfig = z.infer<typeof updateFacebookConfigSchema>;
|
||||
|
||||
export type TiktokConfig = typeof tiktokConfig.$inferSelect;
|
||||
export type InsertTiktokConfig = z.infer<typeof insertTiktokConfigSchema>;
|
||||
export type UpdateTiktokConfig = z.infer<typeof updateTiktokConfigSchema>;
|
||||
|
||||
Reference in new issue
Block a user