feat(tiktok): publier les reels sur les comptes TikTok des magasins

Ajoute la gestion multi-comptes TikTok et permet d'envoyer la même vidéo
sur plusieurs pages Facebook ET plusieurs comptes TikTok en une seule
publication.

Un compte TikTok vit dans social_pages comme une page Facebook : il hérite
donc des permissions par utilisateur, de la planification et du calendrier.

Trois particularités de l'API TikTok structurent l'implémentation :
- pas de jeton saisi à la main : chaque compte passe par OAuth, et
  l'access token (24h) est renouvelé via le refresh token (1 an) avant
  chaque publication ;
- la publication est asynchrone : l'upload rend un publish_id, et un
  poller récupère l'identifiant définitif du post ;
- creator_info doit être interrogé avant chaque envoi pour ne demander
  qu'un niveau de confidentialité réellement autorisé sur le compte.

Serveur :
- service TikTok (OAuth, creator_info, upload FILE_UPLOAD par chunks,
  suivi de statut) et routes de connexion/configuration
- schéma : platform 'tiktok', refresh token et scopes sur social_pages,
  publish_id/publish_status sur scheduled_posts, table tiktok_config
- routage par plateforme dans les deux flux de reels et le planificateur
- cron de suivi des publications, token manager et analytics adaptés

Client :
- connexion et reconnexion des comptes TikTok depuis « Pages gérées »
- sélection combinée pages Facebook / comptes TikTok à la publication
- configuration de l'application TikTok dans les paramètres (admin)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uUPZ1GNpEkYB1dpXHYPek
This commit is contained in:
Claude committed 2026-08-11 14:29:38 +00:00
1 parent 2188fa458d
commit b95cd23ebc
27 files changed
+1679 -183

No files matched your search

+2 -2
View File
@@ -1,7 +1,7 @@
import { format, isSameDay, parseISO } from "date-fns";
import { fr } from "date-fns/locale";
import { Edit, Trash2, ChevronDown, ChevronUp, Eye, Image, Smartphone } from "lucide-react";
import { SiFacebook, SiInstagram } from "react-icons/si";
import { SiFacebook, SiInstagram, SiTiktok } from "react-icons/si";
import { Button } from "@/components/ui/button";
import { useState } from "react";
@@ -159,7 +159,7 @@ export default function CalendarListView({ scheduledPosts, onEditPost, onDeleteP
const isPublished = !!post.publishedAt;
const pageName = post.page?.pageName || 'Page inconnue';
const platform = post.page?.platform || 'facebook';
const PlatformIcon = platform === 'instagram' ? SiInstagram : SiFacebook;
const PlatformIcon = platform === 'instagram' ? SiInstagram : platform === 'tiktok' ? SiTiktok : SiFacebook;
return (
<div
+3 -3
View File
@@ -6,7 +6,7 @@ import { useQuery, useMutation } from "@tanstack/react-query";
import { useToast } from "@/hooks/use-toast";
import { apiRequest, queryClient } from "@/lib/queryClient";
import type { ScheduledPost } from "@shared/schema";
import { SiFacebook, SiInstagram } from "react-icons/si";
import { SiFacebook, SiInstagram, SiTiktok } from "react-icons/si";
import EditScheduledPostDialog from "./edit-scheduled-post-dialog";
import CalendarListView from "./calendar-list-view";
import { useMediaQuery } from "@/hooks/use-media-query";
@@ -261,7 +261,7 @@ export default function CalendarView() {
const isPublished = !!post.publishedAt;
const pageName = post.page?.pageName || 'Page inconnue';
const platform = post.page?.platform || 'facebook';
const PlatformIcon = platform === 'instagram' ? SiInstagram : SiFacebook;
const PlatformIcon = platform === 'instagram' ? SiInstagram : platform === 'tiktok' ? SiTiktok : SiFacebook;
return (
<div
@@ -340,7 +340,7 @@ export default function CalendarView() {
const isPublished = !!post.publishedAt;
const pageName = post.page?.pageName || 'Page inconnue';
const platform = post.page?.platform || 'facebook';
const PlatformIcon = platform === 'instagram' ? SiInstagram : SiFacebook;
const PlatformIcon = platform === 'instagram' ? SiInstagram : platform === 'tiktok' ? SiTiktok : SiFacebook;
return (
<div
+5 -3
View File
@@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { Users } from "lucide-react";
import { SiFacebook, SiInstagram } from "react-icons/si";
import { SiFacebook, SiInstagram, SiTiktok } from "react-icons/si";
export default function ManagedPages() {
const { data: pages, isLoading } = useQuery({
@@ -48,10 +48,12 @@ export default function ManagedPages() {
<div className="flex items-start justify-between mb-5">
<div className={`
w-14 h-14 rounded-xl flex items-center justify-center shadow-md
${page.platform === "facebook" ? "bg-[#1877F2]/10" : "bg-[#E4405F]/10"}
${page.platform === "facebook" ? "bg-[#1877F2]/10" : page.platform === "tiktok" ? "bg-foreground/10" : "bg-[#E4405F]/10"}
`}>
{page.platform === "facebook" ? (
<SiFacebook className="text-[#1877F2] text-2xl" />
) : page.platform === "tiktok" ? (
<SiTiktok className="text-foreground text-2xl" />
) : (
<SiInstagram className="text-[#E4405F] text-2xl" />
)}
@@ -65,7 +67,7 @@ export default function ManagedPages() {
</div>
<h4 className="font-semibold text-foreground text-lg mb-2">{page.pageName}</h4>
<p className="text-sm text-muted-foreground mb-4 font-medium">
{page.platform === "facebook" ? "Facebook Page" : "Instagram Business"}
{page.platform === "facebook" ? "Facebook Page" : page.platform === "tiktok" ? "Compte TikTok" : "Instagram Business"}
</p>
<div className="flex items-center justify-between pt-4 border-t border-border/50">
<div className="flex items-center gap-2">
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Facebook, Instagram, Clock, CheckCircle2, XCircle, Eye, Image as ImageIcon, Smartphone, Clapperboard } from "lucide-react";
import { SiTiktok } from "react-icons/si";
import { format } from "date-fns";
import { fr } from "date-fns/locale";
import type { ScheduledPost, SocialPage, Post, Media } from "@shared/schema";
@@ -162,10 +163,14 @@ export default function RecentPublications() {
{/* Platform icon */}
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${scheduledPost.page?.platform === 'facebook'
? 'bg-blue-500'
: 'bg-gradient-to-br from-purple-500 to-pink-500'
: scheduledPost.page?.platform === 'tiktok'
? 'bg-black'
: 'bg-gradient-to-br from-purple-500 to-pink-500'
}`}>
{scheduledPost.page?.platform === 'facebook' ? (
<Facebook className="w-5 h-5 text-white" />
) : scheduledPost.page?.platform === 'tiktok' ? (
<SiTiktok className="w-5 h-5 text-white" />
) : (
<Instagram className="w-5 h-5 text-white" />
)}