mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-11 17:26:45 +02:00
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
225 lines
7.4 KiB
TypeScript
225 lines
7.4 KiB
TypeScript
import { pool } from "./db";
|
|
|
|
export async function migrate() {
|
|
const client = await pool.connect();
|
|
try {
|
|
console.log("[Migration] Starting safe migration Check...");
|
|
|
|
// 1. Create Enums if not exist and Update Enums
|
|
await client.query(`
|
|
DO $$ BEGIN
|
|
CREATE TYPE "token_status" AS ENUM('valid', 'expiring', 'expired', 'error');
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
`);
|
|
|
|
// Add 'reel' to post_type enum if not exists
|
|
await client.query(`ALTER TYPE "post_type" ADD VALUE IF NOT EXISTS 'reel';`);
|
|
|
|
// Add 'tiktok' to platform enum if not exists
|
|
await client.query(`ALTER TYPE "platform" ADD VALUE IF NOT EXISTS 'tiktok';`);
|
|
|
|
// 2. Create Tables if not exist
|
|
|
|
// post_analytics
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "post_analytics" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"post_id" varchar NOT NULL REFERENCES "posts"("id") ON DELETE cascade,
|
|
"fetched_at" timestamp DEFAULT now(),
|
|
"impressions" integer DEFAULT 0,
|
|
"reach" integer DEFAULT 0,
|
|
"engagement" integer DEFAULT 0,
|
|
"reactions" integer DEFAULT 0,
|
|
"comments" integer DEFAULT 0,
|
|
"shares" integer DEFAULT 0,
|
|
"clicks" integer DEFAULT 0,
|
|
"raw_data" jsonb
|
|
);
|
|
`);
|
|
|
|
// page_analytics_history
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "page_analytics_history" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"page_id" varchar NOT NULL REFERENCES "social_pages"("id") ON DELETE cascade,
|
|
"date" timestamp NOT NULL,
|
|
"followers_count" integer DEFAULT 0,
|
|
"page_views" integer DEFAULT 0,
|
|
"page_reach" integer DEFAULT 0,
|
|
"created_at" timestamp DEFAULT now()
|
|
);
|
|
`);
|
|
|
|
// 3. Add Columns to existing tables if not exist
|
|
|
|
// social_pages.token_status
|
|
await client.query(`
|
|
ALTER TABLE "social_pages"
|
|
ADD COLUMN IF NOT EXISTS "token_status" "token_status" DEFAULT 'valid';
|
|
`);
|
|
|
|
// social_pages.last_token_check
|
|
await client.query(`
|
|
ALTER TABLE "social_pages"
|
|
ADD COLUMN IF NOT EXISTS "last_token_check" timestamp;
|
|
`);
|
|
|
|
// music_favorites
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "music_favorites" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"user_id" varchar NOT NULL REFERENCES "users"("id") ON DELETE cascade,
|
|
"track_id" text NOT NULL,
|
|
"title" text NOT NULL,
|
|
"artist" text NOT NULL,
|
|
"album_name" text,
|
|
"duration" integer NOT NULL,
|
|
"preview_url" text NOT NULL,
|
|
"download_url" text NOT NULL,
|
|
"image_url" text,
|
|
"license" text,
|
|
"created_at" timestamp DEFAULT now()
|
|
);
|
|
`);
|
|
|
|
// freesound_config
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "freesound_config" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"user_id" varchar NOT NULL REFERENCES "users"("id") ON DELETE cascade,
|
|
"client_id" text NOT NULL,
|
|
"client_secret" text NOT NULL,
|
|
"created_at" timestamp DEFAULT now(),
|
|
"updated_at" timestamp DEFAULT now(),
|
|
CONSTRAINT freesound_config_user_id_unique UNIQUE ("user_id")
|
|
);
|
|
`);
|
|
|
|
// posts.generation_status (Reel progress tracking)
|
|
await client.query(`
|
|
ALTER TABLE "posts"
|
|
ADD COLUMN IF NOT EXISTS "generation_status" text;
|
|
`);
|
|
|
|
// posts.generation_progress
|
|
await client.query(`
|
|
ALTER TABLE "posts"
|
|
ADD COLUMN IF NOT EXISTS "generation_progress" integer DEFAULT 0;
|
|
`);
|
|
|
|
// posts.generation_error
|
|
await client.query(`
|
|
ALTER TABLE "posts"
|
|
ADD COLUMN IF NOT EXISTS "generation_error" text;
|
|
`);
|
|
|
|
// cloudinary_config.endpoint_url (MinIO internal endpoint URL)
|
|
await client.query(`
|
|
ALTER TABLE "cloudinary_config"
|
|
ADD COLUMN IF NOT EXISTS "endpoint_url" text;
|
|
`);
|
|
|
|
// cloudinary_config.public_url (MinIO public-facing base URL)
|
|
await client.query(`
|
|
ALTER TABLE "cloudinary_config"
|
|
ADD COLUMN IF NOT EXISTS "public_url" text;
|
|
`);
|
|
// page_analytics_history.page_engagement
|
|
await client.query(`
|
|
ALTER TABLE "page_analytics_history"
|
|
ADD COLUMN IF NOT EXISTS "page_engagement" integer DEFAULT 0;
|
|
`);
|
|
|
|
// audio_tracks (bibliothèque audio interne)
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "audio_tracks" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"user_id" varchar REFERENCES "users"("id") ON DELETE cascade,
|
|
"title" text NOT NULL,
|
|
"file_name" text NOT NULL,
|
|
"url" text NOT NULL,
|
|
"duration" integer DEFAULT 0,
|
|
"created_at" timestamp DEFAULT now()
|
|
);
|
|
`);
|
|
|
|
// app_config (global application settings)
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "app_config" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"external_api_key" text,
|
|
"created_at" timestamp DEFAULT now(),
|
|
"updated_at" timestamp DEFAULT now()
|
|
);
|
|
`);
|
|
|
|
// app_config.gemini_api_key
|
|
await client.query(`
|
|
ALTER TABLE "app_config"
|
|
ADD COLUMN IF NOT EXISTS "gemini_api_key" text;
|
|
`);
|
|
|
|
// media.thumbnail_url (vignette conservée après suppression de la vidéo)
|
|
await client.query(`
|
|
ALTER TABLE "media"
|
|
ADD COLUMN IF NOT EXISTS "thumbnail_url" text;
|
|
`);
|
|
|
|
// tiktok_config (application développeur TikTok, globale)
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "tiktok_config" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"client_key" text NOT NULL,
|
|
"client_secret" text NOT NULL,
|
|
"created_at" timestamp DEFAULT now(),
|
|
"updated_at" timestamp DEFAULT now()
|
|
);
|
|
`);
|
|
|
|
// social_pages : champs OAuth TikTok
|
|
await client.query(`
|
|
ALTER TABLE "social_pages"
|
|
ADD COLUMN IF NOT EXISTS "refresh_token" text,
|
|
ADD COLUMN IF NOT EXISTS "refresh_token_expires_at" timestamp,
|
|
ADD COLUMN IF NOT EXISTS "scopes" text,
|
|
ADD COLUMN IF NOT EXISTS "avatar_url" text;
|
|
`);
|
|
|
|
// facebook_config (application développeur Facebook, globale)
|
|
await client.query(`
|
|
CREATE TABLE IF NOT EXISTS "facebook_config" (
|
|
"id" varchar PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
"app_id" text NOT NULL,
|
|
"app_secret" text NOT NULL,
|
|
"created_at" timestamp DEFAULT now(),
|
|
"updated_at" timestamp DEFAULT now()
|
|
);
|
|
`);
|
|
|
|
// social_pages : renouvellement automatique des tokens Facebook
|
|
await client.query(`
|
|
ALTER TABLE "social_pages"
|
|
ADD COLUMN IF NOT EXISTS "user_access_token" text,
|
|
ADD COLUMN IF NOT EXISTS "user_token_expires_at" timestamp,
|
|
ADD COLUMN IF NOT EXISTS "token_error" text;
|
|
`);
|
|
|
|
// scheduled_posts : suivi de la publication asynchrone TikTok
|
|
await client.query(`
|
|
ALTER TABLE "scheduled_posts"
|
|
ADD COLUMN IF NOT EXISTS "publish_id" text,
|
|
ADD COLUMN IF NOT EXISTS "publish_status" text;
|
|
`);
|
|
|
|
console.log("[Migration] Safe migration completed.");
|
|
} catch (error) {
|
|
console.error("[Migration] Error during migration:", error);
|
|
// Don't kill the process, let the app try to start even if migration failed
|
|
// (It might have failed because tables already existed in a different state)
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|