mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-23 06:58:12 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5550fb05d6 |
No files matched your search
@@ -3,7 +3,7 @@ import { useDropzone } from "react-dropzone";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { MediaThumbnail } from "@/components/media-thumbnail";
|
import { MediaThumbnail } from "@/components/media-thumbnail";
|
||||||
import { CloudUpload, Image as ImageIcon, Video, X, Upload, Loader2, ZoomIn, Camera } from "lucide-react";
|
import { CloudUpload, Image as ImageIcon, Video, X, Upload, Loader2, ZoomIn, Camera } from "lucide-react";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
@@ -30,12 +30,8 @@ export default function MediaUpload() {
|
|||||||
const response = await fetch("/api/media/upload", {
|
const response = await fetch("/api/media/upload", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: "include",
|
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error("Upload failed");
|
||||||
if (response.status === 401) handleUnauthorized("/api/media/upload");
|
|
||||||
throw new Error("Upload failed");
|
|
||||||
}
|
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -59,12 +55,8 @@ export default function MediaUpload() {
|
|||||||
const response = await fetch("/api/media/upload", {
|
const response = await fetch("/api/media/upload", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: "include",
|
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error("Upload failed");
|
||||||
if (response.status === 401) handleUnauthorized("/api/media/upload");
|
|
||||||
throw new Error("Upload failed");
|
|
||||||
}
|
|
||||||
successCount++;
|
successCount++;
|
||||||
return response.json();
|
return response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,30 +1,7 @@
|
|||||||
import { QueryClient, QueryFunction } from "@tanstack/react-query";
|
import { QueryClient, QueryFunction } from "@tanstack/react-query";
|
||||||
|
|
||||||
// Les routes d'authentification elles-mêmes ne doivent jamais déclencher
|
async function throwIfResNotOk(res: Response) {
|
||||||
// de redirection automatique (ex: mauvais mot de passe sur /login).
|
|
||||||
function isAuthRoute(url: string): boolean {
|
|
||||||
return url.startsWith("/api/auth/");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Si la session serveur a expiré (ex: redémarrage du serveur, expiration du
|
|
||||||
// cookie) alors que la SPA pense toujours l'utilisateur connecté (le check
|
|
||||||
// de session initial est mis en cache indéfiniment par react-query), toutes
|
|
||||||
// les requêtes suivantes échouent silencieusement en 401 sans jamais
|
|
||||||
// renvoyer l'utilisateur vers l'écran de connexion. On force donc un
|
|
||||||
// rechargement vers /login dès qu'une requête API (hors routes d'auth)
|
|
||||||
// renvoie 401, ce qui réinitialise aussi le cache react-query.
|
|
||||||
export function handleUnauthorized(url: string) {
|
|
||||||
if (isAuthRoute(url) || window.location.pathname === "/login") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.location.href = "/login";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function throwIfResNotOk(res: Response, url: string) {
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (res.status === 401) {
|
|
||||||
handleUnauthorized(url);
|
|
||||||
}
|
|
||||||
const text = (await res.text()) || res.statusText;
|
const text = (await res.text()) || res.statusText;
|
||||||
throw new Error(`${res.status}: ${text}`);
|
throw new Error(`${res.status}: ${text}`);
|
||||||
}
|
}
|
||||||
@@ -42,7 +19,7 @@ export async function apiRequest(
|
|||||||
credentials: "include",
|
credentials: "include",
|
||||||
});
|
});
|
||||||
|
|
||||||
await throwIfResNotOk(res, url);
|
await throwIfResNotOk(res);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +29,7 @@ export const getQueryFn: <T>(options: {
|
|||||||
}) => QueryFunction<T> =
|
}) => QueryFunction<T> =
|
||||||
({ on401: unauthorizedBehavior }) =>
|
({ on401: unauthorizedBehavior }) =>
|
||||||
async ({ queryKey }) => {
|
async ({ queryKey }) => {
|
||||||
const url = queryKey.join("/") as string;
|
const res = await fetch(queryKey.join("/") as string, {
|
||||||
const res = await fetch(url, {
|
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,7 +37,7 @@ export const getQueryFn: <T>(options: {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await throwIfResNotOk(res, url);
|
await throwIfResNotOk(res);
|
||||||
return await res.json();
|
return await res.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { useToast } from "@/hooks/use-toast";
|
|||||||
import { Trash2, Music, Upload, Loader2, Play, Pause } from "lucide-react";
|
import { Trash2, Music, Upload, Loader2, Play, Pause } from "lucide-react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { fr } from "date-fns/locale";
|
import { fr } from "date-fns/locale";
|
||||||
import { handleUnauthorized } from "@/lib/queryClient";
|
|
||||||
|
|
||||||
interface AudioTrack {
|
interface AudioTrack {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -42,11 +41,9 @@ export default function AudioAdmin() {
|
|||||||
const res = await fetch("/api/audio-tracks", {
|
const res = await fetch("/api/audio-tracks", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: "include",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (res.status === 401) handleUnauthorized("/api/audio-tracks");
|
|
||||||
const error = await res.json();
|
const error = await res.json();
|
||||||
throw new Error(error.error || "Erreur lors de l'upload");
|
throw new Error(error.error || "Erreur lors de l'upload");
|
||||||
}
|
}
|
||||||
@@ -80,11 +77,9 @@ export default function AudioAdmin() {
|
|||||||
mutationFn: async (id: number) => {
|
mutationFn: async (id: number) => {
|
||||||
const res = await fetch(`/api/audio-tracks/${id}`, {
|
const res = await fetch(`/api/audio-tracks/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
credentials: "include",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (res.status === 401) handleUnauthorized(`/api/audio-tracks/${id}`);
|
|
||||||
throw new Error("Erreur lors de la suppression");
|
throw new Error("Erreur lors de la suppression");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import type { SocialPage, Media, ScheduledPost } from "@shared/schema";
|
import type { SocialPage, Media, ScheduledPost } from "@shared/schema";
|
||||||
import { PreviewModal } from "@/components/preview-modal";
|
import { PreviewModal } from "@/components/preview-modal";
|
||||||
import { DateTimePicker } from "@/components/datetime-picker";
|
import { DateTimePicker } from "@/components/datetime-picker";
|
||||||
@@ -152,12 +152,8 @@ export default function NewPostMobile() {
|
|||||||
const response = await fetch("/api/media/upload", {
|
const response = await fetch("/api/media/upload", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: "include",
|
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error("Upload failed");
|
||||||
if (response.status === 401) handleUnauthorized("/api/media/upload");
|
|
||||||
throw new Error("Upload failed");
|
|
||||||
}
|
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
onSuccess: (data: Media) => {
|
onSuccess: (data: Media) => {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import type { SocialPage, Media } from "@shared/schema";
|
import type { SocialPage, Media } from "@shared/schema";
|
||||||
import { DateTimePicker } from "@/components/datetime-picker";
|
import { DateTimePicker } from "@/components/datetime-picker";
|
||||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||||
@@ -145,10 +145,8 @@ export default function MobileNewReel() {
|
|||||||
const response = await fetch("/api/media/upload", {
|
const response = await fetch("/api/media/upload", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: "include",
|
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) handleUnauthorized("/api/media/upload");
|
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
throw new Error(`Upload failed: ${response.status} ${response.statusText} - ${errorText.substring(0, 100)}`);
|
throw new Error(`Upload failed: ${response.status} ${response.statusText} - ${errorText.substring(0, 100)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import Sidebar from "@/components/sidebar";
|
|||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { DateTimePicker } from "@/components/datetime-picker";
|
import { DateTimePicker } from "@/components/datetime-picker";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import type { Media, SocialPage } from "@shared/schema";
|
import type { Media, SocialPage } from "@shared/schema";
|
||||||
|
|
||||||
interface AudioTrack { id: string; title: string; fileName: string; url: string; duration: number; }
|
interface AudioTrack { id: string; title: string; fileName: string; url: string; duration: number; }
|
||||||
@@ -152,8 +152,8 @@ export default function MobileRemotionVideoPage() {
|
|||||||
if (selectedPageIds[0]) formData.append("selectedPageId", selectedPageIds[0]);
|
if (selectedPageIds[0]) formData.append("selectedPageId", selectedPageIds[0]);
|
||||||
if (musicFile) { formData.append("music", musicFile); formData.append("musicVolume", String(musicVolume)); }
|
if (musicFile) { formData.append("music", musicFile); formData.append("musicVolume", String(musicVolume)); }
|
||||||
else if (selectedTrack) { formData.append("musicTrackUrl", selectedTrack.url); formData.append("musicVolume", String(musicVolume)); }
|
else if (selectedTrack) { formData.append("musicTrackUrl", selectedTrack.url); formData.append("musicVolume", String(musicVolume)); }
|
||||||
const response = await fetch("/api/remotion/render", { method: "POST", body: formData, credentials: "include" });
|
const response = await fetch("/api/remotion/render", { method: "POST", body: formData });
|
||||||
if (!response.ok) { if (response.status === 401) handleUnauthorized("/api/remotion/render"); const e = await response.json().catch(() => ({})); throw new Error(e.error || "Erreur"); }
|
if (!response.ok) { const e = await response.json().catch(() => ({})); throw new Error(e.error || "Erreur"); }
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.jobId) {
|
if (data.jobId) {
|
||||||
@@ -161,11 +161,8 @@ export default function MobileRemotionVideoPage() {
|
|||||||
|
|
||||||
const checkStatus = async () => {
|
const checkStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/remotion/render/status/${data.jobId}`, { credentials: "include" });
|
const res = await fetch(`/api/remotion/render/status/${data.jobId}`);
|
||||||
if (!res.ok) {
|
if (!res.ok) throw new Error("Erreur de suivi du rendu");
|
||||||
if (res.status === 401) handleUnauthorized(`/api/remotion/render/status/${data.jobId}`);
|
|
||||||
throw new Error("Erreur de suivi du rendu");
|
|
||||||
}
|
|
||||||
const job = await res.json();
|
const job = await res.json();
|
||||||
|
|
||||||
if (job.status === 'done') {
|
if (job.status === 'done') {
|
||||||
|
|||||||
@@ -157,10 +157,17 @@ export default function SettingsMobile() {
|
|||||||
});
|
});
|
||||||
setOpenrouterApiKey("");
|
setOpenrouterApiKey("");
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (error: Error) => {
|
||||||
|
let description = "Impossible de sauvegarder la configuration OpenRouter";
|
||||||
|
const body = error.message.match(/^\d+:\s*(.+)$/)?.[1];
|
||||||
|
if (body) {
|
||||||
|
try {
|
||||||
|
description = JSON.parse(body).error || description;
|
||||||
|
} catch { /* garder le message générique */ }
|
||||||
|
}
|
||||||
toast({
|
toast({
|
||||||
title: "Erreur",
|
title: "Erreur",
|
||||||
description: "Impossible de sauvegarder la configuration OpenRouter",
|
description,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { Checkbox } from "@/components/ui/checkbox";
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { MediaThumbnail } from "@/components/media-thumbnail";
|
import { MediaThumbnail } from "@/components/media-thumbnail";
|
||||||
import type { SocialPage, Media, ScheduledPost } from "@shared/schema";
|
import type { SocialPage, Media, ScheduledPost } from "@shared/schema";
|
||||||
import { PreviewModal } from "@/components/preview-modal";
|
import { PreviewModal } from "@/components/preview-modal";
|
||||||
@@ -148,12 +148,8 @@ export default function NewPost() {
|
|||||||
const response = await fetch("/api/media/upload", {
|
const response = await fetch("/api/media/upload", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: "include",
|
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error("Upload failed");
|
||||||
if (response.status === 401) handleUnauthorized("/api/media/upload");
|
|
||||||
throw new Error("Upload failed");
|
|
||||||
}
|
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import type { SocialPage, Media } from "@shared/schema";
|
import type { SocialPage, Media } from "@shared/schema";
|
||||||
import { MediaThumbnail } from "@/components/media-thumbnail";
|
import { MediaThumbnail } from "@/components/media-thumbnail";
|
||||||
import { DateTimePicker } from "@/components/datetime-picker";
|
import { DateTimePicker } from "@/components/datetime-picker";
|
||||||
@@ -166,12 +166,8 @@ export default function NewReel() {
|
|||||||
const response = await fetch("/api/media/upload", {
|
const response = await fetch("/api/media/upload", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
credentials: "include",
|
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) throw new Error("Upload failed");
|
||||||
if (response.status === 401) handleUnauthorized("/api/media/upload");
|
|
||||||
throw new Error("Upload failed");
|
|
||||||
}
|
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { UploadCloud, Video, Loader2, Check, Sparkles, Mic, Volume2, Music, Play, Pause, Send } from "lucide-react";
|
import { UploadCloud, Video, Loader2, Check, Sparkles, Mic, Volume2, Music, Play, Pause, Send } from "lucide-react";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||||
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
@@ -119,8 +119,8 @@ export default function RemotionVideoPage() {
|
|||||||
if (selectedPageIds[0]) formData.append("selectedPageId", selectedPageIds[0]);
|
if (selectedPageIds[0]) formData.append("selectedPageId", selectedPageIds[0]);
|
||||||
if (musicFile) { formData.append("music", musicFile); formData.append("musicVolume", String(musicVolume)); }
|
if (musicFile) { formData.append("music", musicFile); formData.append("musicVolume", String(musicVolume)); }
|
||||||
else if (selectedTrack) { formData.append("musicTrackUrl", selectedTrack.url); formData.append("musicVolume", String(musicVolume)); }
|
else if (selectedTrack) { formData.append("musicTrackUrl", selectedTrack.url); formData.append("musicVolume", String(musicVolume)); }
|
||||||
const response = await fetch("/api/remotion/render", { method: "POST", body: formData, credentials: "include" });
|
const response = await fetch("/api/remotion/render", { method: "POST", body: formData });
|
||||||
if (!response.ok) { if (response.status === 401) handleUnauthorized("/api/remotion/render"); const e = await response.json().catch(() => ({})); throw new Error(e.error || "Erreur"); }
|
if (!response.ok) { const e = await response.json().catch(() => ({})); throw new Error(e.error || "Erreur"); }
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.jobId) {
|
if (data.jobId) {
|
||||||
@@ -128,11 +128,8 @@ export default function RemotionVideoPage() {
|
|||||||
|
|
||||||
const checkStatus = async () => {
|
const checkStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/remotion/render/status/${data.jobId}`, { credentials: "include" });
|
const res = await fetch(`/api/remotion/render/status/${data.jobId}`);
|
||||||
if (!res.ok) {
|
if (!res.ok) throw new Error("Erreur de suivi du rendu");
|
||||||
if (res.status === 401) handleUnauthorized(`/api/remotion/render/status/${data.jobId}`);
|
|
||||||
throw new Error("Erreur de suivi du rendu");
|
|
||||||
}
|
|
||||||
const job = await res.json();
|
const job = await res.json();
|
||||||
|
|
||||||
if (job.status === 'done') {
|
if (job.status === 'done') {
|
||||||
|
|||||||
@@ -161,10 +161,17 @@ export default function Settings() {
|
|||||||
});
|
});
|
||||||
setOpenrouterApiKey(""); // Clear the API key after saving
|
setOpenrouterApiKey(""); // Clear the API key after saving
|
||||||
},
|
},
|
||||||
onError: () => {
|
onError: (error: Error) => {
|
||||||
|
let description = "Impossible de sauvegarder la configuration OpenRouter";
|
||||||
|
const body = error.message.match(/^\d+:\s*(.+)$/)?.[1];
|
||||||
|
if (body) {
|
||||||
|
try {
|
||||||
|
description = JSON.parse(body).error || description;
|
||||||
|
} catch { /* garder le message générique */ }
|
||||||
|
}
|
||||||
toast({
|
toast({
|
||||||
title: "Erreur",
|
title: "Erreur",
|
||||||
description: "Impossible de sauvegarder la configuration OpenRouter",
|
description,
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
+13
-1
@@ -1844,6 +1844,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
return res.status(400).json({ error: "Clé API OpenRouter manquante. Veuillez entrer une clé API valide." });
|
return res.status(400).json({ error: "Clé API OpenRouter manquante. Veuillez entrer une clé API valide." });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (incomingKey && !(await openRouterService.verifyApiKey(incomingKey))) {
|
||||||
|
return res.status(400).json({ error: "Clé API refusée par OpenRouter. Vérifiez que vous avez collé la clé complète (sk-or-v1-...)." });
|
||||||
|
}
|
||||||
|
|
||||||
config = await storage.updateOpenrouterConfig(userId, finalData);
|
config = await storage.updateOpenrouterConfig(userId, finalData);
|
||||||
} else {
|
} else {
|
||||||
// Pour les créations, exiger tous les champs
|
// Pour les créations, exiger tous les champs
|
||||||
@@ -1851,14 +1855,22 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
...req.body,
|
...req.body,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!(await openRouterService.verifyApiKey(configData.apiKey))) {
|
||||||
|
return res.status(400).json({ error: "Clé API refusée par OpenRouter. Vérifiez que vous avez collé la clé complète (sk-or-v1-...)." });
|
||||||
|
}
|
||||||
|
|
||||||
config = await storage.createOpenrouterConfig(configData);
|
config = await storage.createOpenrouterConfig(configData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't send the API key back
|
// Don't send the API key back
|
||||||
const { apiKey, ...safeConfig } = config;
|
const { apiKey, ...safeConfig } = config;
|
||||||
res.json(safeConfig);
|
res.json(safeConfig);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Error saving OpenRouter config:", error);
|
console.error("Error saving OpenRouter config:", error);
|
||||||
|
if (error.name === 'ZodError') {
|
||||||
|
return res.status(400).json({ error: error.errors[0]?.message || "Données invalides" });
|
||||||
|
}
|
||||||
res.status(500).json({ error: "Failed to save OpenRouter config" });
|
res.status(500).json({ error: "Failed to save OpenRouter config" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -215,6 +215,22 @@ VERSION 3 - ÉMOTIONNELLE:
|
|||||||
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie qu'une clé API est acceptée par OpenRouter.
|
||||||
|
* Retourne false uniquement sur un rejet d'authentification explicite (401/403) ;
|
||||||
|
* en cas d'erreur réseau, on laisse passer pour ne pas bloquer la sauvegarde.
|
||||||
|
*/
|
||||||
|
async verifyApiKey(apiKey: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://openrouter.ai/api/v1/key", {
|
||||||
|
headers: { "Authorization": `Bearer ${apiKey}` }
|
||||||
|
});
|
||||||
|
return response.status !== 401 && response.status !== 403;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getAvailableModels(): Promise<any[]> {
|
async getAvailableModels(): Promise<any[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("https://openrouter.ai/api/v1/models", {
|
const response = await fetch("https://openrouter.ai/api/v1/models", {
|
||||||
|
|||||||
+1
-1
@@ -394,7 +394,7 @@ export const insertOpenrouterConfigSchema = createInsertSchema(openrouterConfig)
|
|||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
}).extend({
|
}).extend({
|
||||||
apiKey: z.string().trim().min(1, "La clé API ne peut pas être vide"),
|
apiKey: z.string().trim().min(20, "La clé API OpenRouter semble incomplète. Collez la clé entière (elle commence par sk-or-v1- et fait environ 73 caractères)."),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateOpenrouterConfigSchema = insertOpenrouterConfigSchema.partial({
|
export const updateOpenrouterConfigSchema = insertOpenrouterConfigSchema.partial({
|
||||||
|
|||||||
Reference in new issue
Block a user