mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-15 11:18:54 +02:00
feat: add external API for publishing posts with image URL and scheduling
- New POST /api/v1/publish endpoint: download image from URL, create post and schedule it per page - New GET /api/v1/pages endpoint: list available pages for external callers - API key auth via X-API-Key header, configurable from admin settings (stored in DB) - New app_config table (migration included) to store the external API key - Admin-only settings section (desktop + mobile) to set/revoke the key - Fallback to EXTERNAL_API_KEY env var if no DB key is configured Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
9e6fe9fe31
commit
391acfdb83
9 files changed
+539
-2
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Settings as SettingsIcon, Bell, Brain, Cloud, Image, Upload, X } from "lucide-react";
|
||||
import { Settings as SettingsIcon, Bell, Brain, Cloud, Image, Upload, X, Plug, Key } from "lucide-react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import TopBar from "@/components/topbar";
|
||||
@@ -27,6 +27,7 @@ export default function SettingsMobile() {
|
||||
const [openrouterSystemPrompt, setOpenrouterSystemPrompt] = useState("Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement.");
|
||||
const [logoFile, setLogoFile] = useState<File | null>(null);
|
||||
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
||||
const [externalApiKey, setExternalApiKey] = useState("");
|
||||
const { toast } = useToast();
|
||||
|
||||
const { data: session } = useQuery<{ id: string; username: string; role: string }>({
|
||||
@@ -70,6 +71,36 @@ export default function SettingsMobile() {
|
||||
|
||||
const hasExistingConfig = !!openrouterConfig;
|
||||
|
||||
const { data: externalApiConfig } = useQuery({
|
||||
queryKey: ['/api/settings/external-api'],
|
||||
enabled: isAdmin,
|
||||
});
|
||||
|
||||
const hasExistingExternalApiConfig = !!(externalApiConfig as any)?.configured;
|
||||
|
||||
const saveExternalApiMutation = useMutation({
|
||||
mutationFn: () => apiRequest('POST', '/api/settings/external-api', { apiKey: externalApiKey }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/settings/external-api'] });
|
||||
setExternalApiKey("");
|
||||
toast({ title: "Clé API sauvegardée", description: "La clé API externe a été enregistrée" });
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: "Erreur", description: "Impossible de sauvegarder la clé API", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const deleteExternalApiMutation = useMutation({
|
||||
mutationFn: () => apiRequest('DELETE', '/api/settings/external-api', {}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/settings/external-api'] });
|
||||
toast({ title: "Clé API supprimée", description: "L'accès à l'API externe a été révoqué" });
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: "Erreur", description: "Impossible de supprimer la clé API", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const saveCloudinaryMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
const payload: any = {
|
||||
@@ -489,6 +520,61 @@ export default function SettingsMobile() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{/* API externe */}
|
||||
{isAdmin && (
|
||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||
<CardHeader className="p-5">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Plug className="w-5 h-5" />
|
||||
API externe
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs">
|
||||
Clé d'accès pour créer et programmer des publications via l'API externe
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 p-5 pt-0">
|
||||
{hasExistingExternalApiConfig && (
|
||||
<div className="rounded-lg bg-muted p-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
|
||||
<Key className="w-4 h-4" />
|
||||
<span>Clé active</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => deleteExternalApiMutation.mutate()}
|
||||
disabled={deleteExternalApiMutation.isPending}
|
||||
>
|
||||
{deleteExternalApiMutation.isPending ? "..." : "Révoquer"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="externalApiKeyMobile">
|
||||
{hasExistingExternalApiConfig ? "Nouvelle clé (remplace l'actuelle)" : "Clé API"}
|
||||
</Label>
|
||||
<Input
|
||||
id="externalApiKeyMobile"
|
||||
type="password"
|
||||
value={externalApiKey}
|
||||
onChange={(e) => setExternalApiKey(e.target.value)}
|
||||
placeholder={hasExistingExternalApiConfig ? "Nouvelle clé..." : "Entrez une clé sécurisée"}
|
||||
className="min-h-[48px]"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Header <code className="bg-muted px-1 rounded">X-API-Key</code> dans vos requêtes
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => saveExternalApiMutation.mutate()}
|
||||
disabled={saveExternalApiMutation.isPending || !externalApiKey.trim()}
|
||||
className="w-full min-h-[48px]"
|
||||
>
|
||||
{saveExternalApiMutation.isPending ? "Enregistrement..." : hasExistingExternalApiConfig ? "Remplacer la clé" : "Enregistrer la clé"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Settings as SettingsIcon, Bell, Key, Shield, Cloud, Brain, Image, Upload, X, Video } from "lucide-react";
|
||||
import { Settings as SettingsIcon, Bell, Key, Shield, Cloud, Brain, Image, Upload, X, Video, Plug } from "lucide-react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import Sidebar from "@/components/sidebar";
|
||||
import TopBar from "@/components/topbar";
|
||||
@@ -29,6 +29,7 @@ export default function Settings() {
|
||||
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
||||
const [ffmpegApiUrl, setFfmpegApiUrl] = useState("");
|
||||
const [ffmpegApiKey, setFfmpegApiKey] = useState("");
|
||||
const [externalApiKey, setExternalApiKey] = useState("");
|
||||
const { toast } = useToast();
|
||||
|
||||
const { data: session } = useQuery<{ id: string; username: string; role: string }>({
|
||||
@@ -54,6 +55,11 @@ export default function Settings() {
|
||||
queryKey: ['/api/ffmpeg/config'],
|
||||
});
|
||||
|
||||
const { data: externalApiConfig } = useQuery({
|
||||
queryKey: ['/api/settings/external-api'],
|
||||
enabled: isAdmin,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (cloudinaryConfig) {
|
||||
setCloudName((cloudinaryConfig as any).cloudName || "");
|
||||
@@ -251,6 +257,31 @@ export default function Settings() {
|
||||
},
|
||||
});
|
||||
|
||||
const hasExistingExternalApiConfig = !!(externalApiConfig as any)?.configured;
|
||||
|
||||
const saveExternalApiMutation = useMutation({
|
||||
mutationFn: () => apiRequest('POST', '/api/settings/external-api', { apiKey: externalApiKey }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/settings/external-api'] });
|
||||
setExternalApiKey("");
|
||||
toast({ title: "Clé API sauvegardée", description: "La clé API externe a été enregistrée" });
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: "Erreur", description: "Impossible de sauvegarder la clé API", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const deleteExternalApiMutation = useMutation({
|
||||
mutationFn: () => apiRequest('DELETE', '/api/settings/external-api', {}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/api/settings/external-api'] });
|
||||
toast({ title: "Clé API supprimée", description: "L'accès à l'API externe a été révoqué" });
|
||||
},
|
||||
onError: () => {
|
||||
toast({ title: "Erreur", description: "Impossible de supprimer la clé API", variant: "destructive" });
|
||||
},
|
||||
});
|
||||
|
||||
const saveFfmpegMutation = useMutation({
|
||||
mutationFn: () => {
|
||||
const payload: any = {
|
||||
@@ -689,6 +720,60 @@ export default function Settings() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||
<CardHeader className="p-6">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Plug className="w-5 h-5" />
|
||||
API externe
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Clé d'accès pour créer et programmer des publications via l'API externe (<code className="text-xs bg-muted px-1 rounded">POST /api/v1/publish</code>)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{hasExistingExternalApiConfig && (
|
||||
<div className="rounded-lg bg-muted p-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
|
||||
<Key className="w-4 h-4" />
|
||||
<span>Clé API configurée et active</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => deleteExternalApiMutation.mutate()}
|
||||
disabled={deleteExternalApiMutation.isPending}
|
||||
>
|
||||
{deleteExternalApiMutation.isPending ? "Suppression..." : "Révoquer"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="externalApiKey">
|
||||
{hasExistingExternalApiConfig ? "Nouvelle clé API (remplace l'actuelle)" : "Clé API"}
|
||||
</Label>
|
||||
<Input
|
||||
id="externalApiKey"
|
||||
type="password"
|
||||
value={externalApiKey}
|
||||
onChange={(e) => setExternalApiKey(e.target.value)}
|
||||
placeholder={hasExistingExternalApiConfig ? "Nouvelle clé pour remplacer l'actuelle" : "Générez une clé sécurisée"}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Utilisez cette clé dans le header <code className="bg-muted px-1 rounded">X-API-Key</code> de vos requêtes
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => saveExternalApiMutation.mutate()}
|
||||
disabled={saveExternalApiMutation.isPending || !externalApiKey.trim()}
|
||||
className="w-full"
|
||||
>
|
||||
{saveExternalApiMutation.isPending ? "Enregistrement..." : hasExistingExternalApiConfig ? "Remplacer la clé" : "Enregistrer la clé"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||
<CardHeader className="p-6">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
|
||||
Reference in new issue
Block a user