mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-23 23:18:12 +02:00
feat: Implement analytics, media management, and third-party service configurations with new settings pages.
This commit is contained in:
1 parent
cb0ff8a0ea
commit
82befb2264
5 files changed
+354
-357
No files matched your search
@@ -27,6 +27,13 @@ export default function SettingsMobile() {
|
|||||||
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
const [logoPreview, setLogoPreview] = useState<string | null>(null);
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const { data: session } = useQuery<{ id: string; username: string; role: string }>({
|
||||||
|
queryKey: ["/api/auth/session"],
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAdmin = session?.role === "admin";
|
||||||
|
|
||||||
const { data: cloudinaryConfig } = useQuery({
|
const { data: cloudinaryConfig } = useQuery({
|
||||||
queryKey: ['/api/cloudinary/config'],
|
queryKey: ['/api/cloudinary/config'],
|
||||||
});
|
});
|
||||||
@@ -355,169 +362,173 @@ export default function SettingsMobile() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Cloudinary */}
|
{/* Cloudinary */}
|
||||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
{isAdmin && (
|
||||||
<CardHeader className="p-5">
|
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<CardHeader className="p-5">
|
||||||
<Cloud className="w-5 h-5" />
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
Configuration Cloudinary
|
<Cloud className="w-5 h-5" />
|
||||||
</CardTitle>
|
Configuration Cloudinary
|
||||||
<CardDescription className="text-xs">
|
</CardTitle>
|
||||||
Configurez Cloudinary pour le stockage d'images dans le cloud
|
<CardDescription className="text-xs">
|
||||||
</CardDescription>
|
Configurez Cloudinary pour le stockage d'images dans le cloud
|
||||||
</CardHeader>
|
</CardDescription>
|
||||||
<CardContent className="space-y-4 p-5 pt-0">
|
</CardHeader>
|
||||||
<div className="space-y-2">
|
<CardContent className="space-y-4 p-5 pt-0">
|
||||||
<Label htmlFor="cloudName" className="text-sm">Cloud Name</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label htmlFor="cloudName" className="text-sm">Cloud Name</Label>
|
||||||
id="cloudName"
|
<Input
|
||||||
type="text"
|
id="cloudName"
|
||||||
value={cloudName}
|
type="text"
|
||||||
onChange={(e) => setCloudName(e.target.value)}
|
value={cloudName}
|
||||||
placeholder="mon-cloud-name"
|
onChange={(e) => setCloudName(e.target.value)}
|
||||||
className="min-h-[48px]"
|
placeholder="mon-cloud-name"
|
||||||
data-testid="input-cloud-name"
|
className="min-h-[48px]"
|
||||||
/>
|
data-testid="input-cloud-name"
|
||||||
<p className="text-xs text-muted-foreground">
|
/>
|
||||||
Trouvez-le dans votre tableau de bord Cloudinary
|
<p className="text-xs text-muted-foreground">
|
||||||
</p>
|
Trouvez-le dans votre tableau de bord Cloudinary
|
||||||
</div>
|
</p>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label htmlFor="apiKey" className="text-sm">API Key</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label htmlFor="apiKey" className="text-sm">API Key</Label>
|
||||||
id="apiKey"
|
<Input
|
||||||
type="text"
|
id="apiKey"
|
||||||
value={apiKey}
|
type="text"
|
||||||
onChange={(e) => setApiKey(e.target.value)}
|
value={apiKey}
|
||||||
placeholder="123456789012345"
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
className="min-h-[48px]"
|
placeholder="123456789012345"
|
||||||
data-testid="input-api-key"
|
className="min-h-[48px]"
|
||||||
/>
|
data-testid="input-api-key"
|
||||||
</div>
|
/>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label htmlFor="apiSecret" className="text-sm">API Secret</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label htmlFor="apiSecret" className="text-sm">API Secret</Label>
|
||||||
id="apiSecret"
|
<Input
|
||||||
type="password"
|
id="apiSecret"
|
||||||
value={apiSecret}
|
type="password"
|
||||||
onChange={(e) => setApiSecret(e.target.value)}
|
value={apiSecret}
|
||||||
placeholder="••••••••••••••••"
|
onChange={(e) => setApiSecret(e.target.value)}
|
||||||
className="min-h-[48px]"
|
placeholder="••••••••••••••••"
|
||||||
data-testid="input-api-secret"
|
className="min-h-[48px]"
|
||||||
/>
|
data-testid="input-api-secret"
|
||||||
<p className="text-xs text-muted-foreground">
|
/>
|
||||||
Votre API Secret restera sécurisé et ne sera jamais exposé
|
<p className="text-xs text-muted-foreground">
|
||||||
</p>
|
Votre API Secret restera sécurisé et ne sera jamais exposé
|
||||||
</div>
|
</p>
|
||||||
<Button
|
</div>
|
||||||
onClick={() => saveCloudinaryMutation.mutate()}
|
<Button
|
||||||
disabled={saveCloudinaryMutation.isPending || !cloudName || !apiKey || !apiSecret}
|
onClick={() => saveCloudinaryMutation.mutate()}
|
||||||
data-testid="button-save-cloudinary"
|
disabled={saveCloudinaryMutation.isPending || !cloudName || !apiKey || !apiSecret}
|
||||||
className="w-full min-h-[48px]"
|
data-testid="button-save-cloudinary"
|
||||||
>
|
className="w-full min-h-[48px]"
|
||||||
{saveCloudinaryMutation.isPending ? "Enregistrement..." : "Enregistrer Cloudinary"}
|
>
|
||||||
</Button>
|
{saveCloudinaryMutation.isPending ? "Enregistrement..." : "Enregistrer Cloudinary"}
|
||||||
</CardContent>
|
</Button>
|
||||||
</Card>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
{isAdmin && (
|
||||||
<CardHeader className="p-5">
|
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||||
<CardTitle className="flex items-center gap-2 text-base">
|
<CardHeader className="p-5">
|
||||||
<Image className="w-5 h-5" />
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
Logo de l'entreprise
|
<Image className="w-5 h-5" />
|
||||||
</CardTitle>
|
Logo de l'entreprise
|
||||||
<CardDescription className="text-xs">
|
</CardTitle>
|
||||||
Uploadez votre logo pour l'ajouter aux images éditées
|
<CardDescription className="text-xs">
|
||||||
</CardDescription>
|
Uploadez votre logo pour l'ajouter aux images éditées
|
||||||
</CardHeader>
|
</CardDescription>
|
||||||
<CardContent className="space-y-4 p-5 pt-0">
|
</CardHeader>
|
||||||
{logoPreview ? (
|
<CardContent className="space-y-4 p-5 pt-0">
|
||||||
<div className="space-y-4">
|
{logoPreview ? (
|
||||||
<div className="relative w-full max-w-xs mx-auto">
|
<div className="space-y-4">
|
||||||
<img
|
<div className="relative w-full max-w-xs mx-auto">
|
||||||
src={logoPreview}
|
<img
|
||||||
alt="Logo"
|
src={logoPreview}
|
||||||
className="w-full h-auto max-h-48 object-contain rounded-lg border-2 border-border"
|
alt="Logo"
|
||||||
/>
|
className="w-full h-auto max-h-48 object-contain rounded-lg border-2 border-border"
|
||||||
<Button
|
/>
|
||||||
size="icon"
|
|
||||||
variant="destructive"
|
|
||||||
className="absolute top-2 right-2 h-10 w-10"
|
|
||||||
onClick={() => deleteLogoMutation.mutate()}
|
|
||||||
disabled={deleteLogoMutation.isPending}
|
|
||||||
data-testid="button-delete-logo"
|
|
||||||
>
|
|
||||||
<X className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="logo-upload-replace" className="cursor-pointer">
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
size="icon"
|
||||||
className="w-full min-h-[48px]"
|
variant="destructive"
|
||||||
disabled={uploadLogoMutation.isPending}
|
className="absolute top-2 right-2 h-10 w-10"
|
||||||
data-testid="button-replace-logo"
|
onClick={() => deleteLogoMutation.mutate()}
|
||||||
onClick={() => document.getElementById('logo-upload-replace')?.click()}
|
disabled={deleteLogoMutation.isPending}
|
||||||
|
data-testid="button-delete-logo"
|
||||||
>
|
>
|
||||||
<Upload className="w-5 h-5 mr-2" />
|
<X className="h-5 w-5" />
|
||||||
{uploadLogoMutation.isPending ? "Upload en cours..." : "Remplacer le logo"}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Label>
|
</div>
|
||||||
<input
|
<div>
|
||||||
id="logo-upload-replace"
|
<Label htmlFor="logo-upload-replace" className="cursor-pointer">
|
||||||
type="file"
|
<Button
|
||||||
accept="image/*"
|
variant="outline"
|
||||||
className="hidden"
|
className="w-full min-h-[48px]"
|
||||||
onChange={(e) => {
|
disabled={uploadLogoMutation.isPending}
|
||||||
handleLogoChange(e);
|
data-testid="button-replace-logo"
|
||||||
if (e.target.files?.[0]) {
|
onClick={() => document.getElementById('logo-upload-replace')?.click()}
|
||||||
uploadLogoMutation.mutate(e.target.files[0]);
|
>
|
||||||
}
|
<Upload className="w-5 h-5 mr-2" />
|
||||||
}}
|
{uploadLogoMutation.isPending ? "Upload en cours..." : "Remplacer le logo"}
|
||||||
/>
|
</Button>
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
id="logo-upload-replace"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
handleLogoChange(e);
|
||||||
|
if (e.target.files?.[0]) {
|
||||||
|
uploadLogoMutation.mutate(e.target.files[0]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<div className="space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="border-2 border-dashed border-border rounded-lg p-8 text-center">
|
||||||
<div className="border-2 border-dashed border-border rounded-lg p-8 text-center">
|
<Upload className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
|
||||||
<Upload className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
Aucun logo uploadé. Choisissez une image PNG ou JPG.
|
||||||
Aucun logo uploadé. Choisissez une image PNG ou JPG.
|
</p>
|
||||||
</p>
|
<Label htmlFor="logo-upload" className="cursor-pointer">
|
||||||
<Label htmlFor="logo-upload" className="cursor-pointer">
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
disabled={uploadLogoMutation.isPending || !cloudinaryConfig}
|
||||||
disabled={uploadLogoMutation.isPending || !cloudinaryConfig}
|
className="min-h-[48px]"
|
||||||
className="min-h-[48px]"
|
data-testid="button-upload-logo"
|
||||||
data-testid="button-upload-logo"
|
onClick={() => document.getElementById('logo-upload')?.click()}
|
||||||
onClick={() => document.getElementById('logo-upload')?.click()}
|
>
|
||||||
>
|
<Upload className="w-5 h-5 mr-2" />
|
||||||
<Upload className="w-5 h-5 mr-2" />
|
{uploadLogoMutation.isPending ? "Upload en cours..." : "Uploader un logo"}
|
||||||
{uploadLogoMutation.isPending ? "Upload en cours..." : "Uploader un logo"}
|
</Button>
|
||||||
</Button>
|
</Label>
|
||||||
</Label>
|
<input
|
||||||
<input
|
id="logo-upload"
|
||||||
id="logo-upload"
|
type="file"
|
||||||
type="file"
|
accept="image/*"
|
||||||
accept="image/*"
|
className="hidden"
|
||||||
className="hidden"
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
handleLogoChange(e);
|
||||||
handleLogoChange(e);
|
if (e.target.files?.[0]) {
|
||||||
if (e.target.files?.[0]) {
|
uploadLogoMutation.mutate(e.target.files[0]);
|
||||||
uploadLogoMutation.mutate(e.target.files[0]);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</div>
|
||||||
|
{!cloudinaryConfig && (
|
||||||
|
<p className="text-xs text-orange-600 dark:text-orange-400 text-center">
|
||||||
|
Veuillez d'abord configurer Cloudinary ci-dessus
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!cloudinaryConfig && (
|
)}
|
||||||
<p className="text-xs text-orange-600 dark:text-orange-400 text-center">
|
</CardContent>
|
||||||
Veuillez d'abord configurer Cloudinary ci-dessus
|
</Card>
|
||||||
</p>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+163
-152
@@ -29,6 +29,13 @@ export default function Settings() {
|
|||||||
const [ffmpegApiKey, setFfmpegApiKey] = useState("");
|
const [ffmpegApiKey, setFfmpegApiKey] = useState("");
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const { data: session } = useQuery<{ id: string; username: string; role: string }>({
|
||||||
|
queryKey: ["/api/auth/session"],
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAdmin = session?.role === "admin";
|
||||||
|
|
||||||
const { data: cloudinaryConfig } = useQuery({
|
const { data: cloudinaryConfig } = useQuery({
|
||||||
queryKey: ['/api/cloudinary/config'],
|
queryKey: ['/api/cloudinary/config'],
|
||||||
});
|
});
|
||||||
@@ -448,66 +455,68 @@ export default function Settings() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
{isAdmin && (
|
||||||
<CardHeader className="p-6">
|
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardHeader className="p-6">
|
||||||
<Cloud className="w-5 h-5" />
|
<CardTitle className="flex items-center gap-2">
|
||||||
Configuration Cloudinary
|
<Cloud className="w-5 h-5" />
|
||||||
</CardTitle>
|
Configuration Cloudinary
|
||||||
<CardDescription>
|
</CardTitle>
|
||||||
Configurez Cloudinary pour le stockage d'images dans le cloud
|
<CardDescription>
|
||||||
</CardDescription>
|
Configurez Cloudinary pour le stockage d'images dans le cloud
|
||||||
</CardHeader>
|
</CardDescription>
|
||||||
<CardContent className="space-y-4">
|
</CardHeader>
|
||||||
<div className="space-y-2">
|
<CardContent className="space-y-4">
|
||||||
<Label htmlFor="cloudName">Cloud Name</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label htmlFor="cloudName">Cloud Name</Label>
|
||||||
id="cloudName"
|
<Input
|
||||||
type="text"
|
id="cloudName"
|
||||||
value={cloudName}
|
type="text"
|
||||||
onChange={(e) => setCloudName(e.target.value)}
|
value={cloudName}
|
||||||
placeholder="mon-cloud-name"
|
onChange={(e) => setCloudName(e.target.value)}
|
||||||
data-testid="input-cloud-name"
|
placeholder="mon-cloud-name"
|
||||||
/>
|
data-testid="input-cloud-name"
|
||||||
<p className="text-xs text-muted-foreground">
|
/>
|
||||||
Trouvez-le dans votre tableau de bord Cloudinary
|
<p className="text-xs text-muted-foreground">
|
||||||
</p>
|
Trouvez-le dans votre tableau de bord Cloudinary
|
||||||
</div>
|
</p>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label htmlFor="apiKey">API Key</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label htmlFor="apiKey">API Key</Label>
|
||||||
id="apiKey"
|
<Input
|
||||||
type="text"
|
id="apiKey"
|
||||||
value={apiKey}
|
type="text"
|
||||||
onChange={(e) => setApiKey(e.target.value)}
|
value={apiKey}
|
||||||
placeholder="123456789012345"
|
onChange={(e) => setApiKey(e.target.value)}
|
||||||
data-testid="input-api-key"
|
placeholder="123456789012345"
|
||||||
/>
|
data-testid="input-api-key"
|
||||||
</div>
|
/>
|
||||||
<div className="space-y-2">
|
</div>
|
||||||
<Label htmlFor="apiSecret">API Secret</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label htmlFor="apiSecret">API Secret</Label>
|
||||||
id="apiSecret"
|
<Input
|
||||||
type="password"
|
id="apiSecret"
|
||||||
value={apiSecret}
|
type="password"
|
||||||
onChange={(e) => setApiSecret(e.target.value)}
|
value={apiSecret}
|
||||||
placeholder="••••••••••••••••"
|
onChange={(e) => setApiSecret(e.target.value)}
|
||||||
data-testid="input-api-secret"
|
placeholder="••••••••••••••••"
|
||||||
/>
|
data-testid="input-api-secret"
|
||||||
<p className="text-xs text-muted-foreground">
|
/>
|
||||||
Votre API Secret restera sécurisé et ne sera jamais exposé
|
<p className="text-xs text-muted-foreground">
|
||||||
</p>
|
Votre API Secret restera sécurisé et ne sera jamais exposé
|
||||||
</div>
|
</p>
|
||||||
<Button
|
</div>
|
||||||
onClick={() => saveCloudinaryMutation.mutate()}
|
<Button
|
||||||
disabled={saveCloudinaryMutation.isPending || !cloudName || !apiKey || !apiSecret}
|
onClick={() => saveCloudinaryMutation.mutate()}
|
||||||
data-testid="button-save-cloudinary"
|
disabled={saveCloudinaryMutation.isPending || !cloudName || !apiKey || !apiSecret}
|
||||||
className="w-full"
|
data-testid="button-save-cloudinary"
|
||||||
>
|
className="w-full"
|
||||||
{saveCloudinaryMutation.isPending ? "Enregistrement..." : "Enregistrer Cloudinary"}
|
>
|
||||||
</Button>
|
{saveCloudinaryMutation.isPending ? "Enregistrement..." : "Enregistrer Cloudinary"}
|
||||||
</CardContent>
|
</Button>
|
||||||
</Card>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||||
<CardHeader className="p-6">
|
<CardHeader className="p-6">
|
||||||
@@ -623,103 +632,105 @@ export default function Settings() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
{isAdmin && (
|
||||||
<CardHeader className="p-6">
|
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardHeader className="p-6">
|
||||||
<Image className="w-5 h-5" />
|
<CardTitle className="flex items-center gap-2">
|
||||||
Logo de l'entreprise
|
<Image className="w-5 h-5" />
|
||||||
</CardTitle>
|
Logo de l'entreprise
|
||||||
<CardDescription>
|
</CardTitle>
|
||||||
Uploadez votre logo pour l'ajouter aux images éditées
|
<CardDescription>
|
||||||
</CardDescription>
|
Uploadez votre logo pour l'ajouter aux images éditées
|
||||||
</CardHeader>
|
</CardDescription>
|
||||||
<CardContent className="space-y-4">
|
</CardHeader>
|
||||||
{logoPreview ? (
|
<CardContent className="space-y-4">
|
||||||
<div className="space-y-4">
|
{logoPreview ? (
|
||||||
<div className="relative w-full max-w-xs mx-auto">
|
<div className="space-y-4">
|
||||||
<img
|
<div className="relative w-full max-w-xs mx-auto">
|
||||||
src={logoPreview}
|
<img
|
||||||
alt="Logo"
|
src={logoPreview}
|
||||||
className="w-full h-auto max-h-48 object-contain rounded-lg border-2 border-border"
|
alt="Logo"
|
||||||
/>
|
className="w-full h-auto max-h-48 object-contain rounded-lg border-2 border-border"
|
||||||
<Button
|
/>
|
||||||
size="icon"
|
|
||||||
variant="destructive"
|
|
||||||
className="absolute top-2 right-2 h-8 w-8"
|
|
||||||
onClick={() => deleteLogoMutation.mutate()}
|
|
||||||
disabled={deleteLogoMutation.isPending}
|
|
||||||
data-testid="button-delete-logo"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<Label htmlFor="logo-upload-replace" className="cursor-pointer">
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
size="icon"
|
||||||
className="w-full"
|
variant="destructive"
|
||||||
disabled={uploadLogoMutation.isPending}
|
className="absolute top-2 right-2 h-8 w-8"
|
||||||
data-testid="button-replace-logo"
|
onClick={() => deleteLogoMutation.mutate()}
|
||||||
onClick={() => document.getElementById('logo-upload-replace')?.click()}
|
disabled={deleteLogoMutation.isPending}
|
||||||
|
data-testid="button-delete-logo"
|
||||||
>
|
>
|
||||||
<Upload className="w-4 h-4 mr-2" />
|
<X className="h-4 w-4" />
|
||||||
{uploadLogoMutation.isPending ? "Upload en cours..." : "Remplacer le logo"}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Label>
|
</div>
|
||||||
<input
|
<div className="text-center">
|
||||||
id="logo-upload-replace"
|
<Label htmlFor="logo-upload-replace" className="cursor-pointer">
|
||||||
type="file"
|
<Button
|
||||||
accept="image/*"
|
variant="outline"
|
||||||
className="hidden"
|
className="w-full"
|
||||||
onChange={(e) => {
|
disabled={uploadLogoMutation.isPending}
|
||||||
handleLogoChange(e);
|
data-testid="button-replace-logo"
|
||||||
if (e.target.files?.[0]) {
|
onClick={() => document.getElementById('logo-upload-replace')?.click()}
|
||||||
uploadLogoMutation.mutate(e.target.files[0]);
|
>
|
||||||
}
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
}}
|
{uploadLogoMutation.isPending ? "Upload en cours..." : "Remplacer le logo"}
|
||||||
/>
|
</Button>
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
id="logo-upload-replace"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
handleLogoChange(e);
|
||||||
|
if (e.target.files?.[0]) {
|
||||||
|
uploadLogoMutation.mutate(e.target.files[0]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<div className="space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="border-2 border-dashed border-border rounded-lg p-8 text-center">
|
||||||
<div className="border-2 border-dashed border-border rounded-lg p-8 text-center">
|
<Upload className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
|
||||||
<Upload className="w-12 h-12 mx-auto mb-4 text-muted-foreground" />
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
Aucun logo uploadé. Choisissez une image PNG ou JPG.
|
||||||
Aucun logo uploadé. Choisissez une image PNG ou JPG.
|
</p>
|
||||||
</p>
|
<Label htmlFor="logo-upload" className="cursor-pointer">
|
||||||
<Label htmlFor="logo-upload" className="cursor-pointer">
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
disabled={uploadLogoMutation.isPending || !cloudinaryConfig}
|
||||||
disabled={uploadLogoMutation.isPending || !cloudinaryConfig}
|
data-testid="button-upload-logo"
|
||||||
data-testid="button-upload-logo"
|
onClick={() => document.getElementById('logo-upload')?.click()}
|
||||||
onClick={() => document.getElementById('logo-upload')?.click()}
|
>
|
||||||
>
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
<Upload className="w-4 h-4 mr-2" />
|
{uploadLogoMutation.isPending ? "Upload en cours..." : "Uploader un logo"}
|
||||||
{uploadLogoMutation.isPending ? "Upload en cours..." : "Uploader un logo"}
|
</Button>
|
||||||
</Button>
|
</Label>
|
||||||
</Label>
|
<input
|
||||||
<input
|
id="logo-upload"
|
||||||
id="logo-upload"
|
type="file"
|
||||||
type="file"
|
accept="image/*"
|
||||||
accept="image/*"
|
className="hidden"
|
||||||
className="hidden"
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
handleLogoChange(e);
|
||||||
handleLogoChange(e);
|
if (e.target.files?.[0]) {
|
||||||
if (e.target.files?.[0]) {
|
uploadLogoMutation.mutate(e.target.files[0]);
|
||||||
uploadLogoMutation.mutate(e.target.files[0]);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</div>
|
||||||
|
{!cloudinaryConfig && (
|
||||||
|
<p className="text-xs text-orange-600 dark:text-orange-400 text-center">
|
||||||
|
Veuillez d'abord configurer Cloudinary ci-dessus
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!cloudinaryConfig && (
|
)}
|
||||||
<p className="text-xs text-orange-600 dark:text-orange-400 text-center">
|
</CardContent>
|
||||||
Veuillez d'abord configurer Cloudinary ci-dessus
|
</Card>
|
||||||
</p>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="rounded-2xl border-border/50 shadow-lg">
|
<Card className="rounded-2xl border-border/50 shadow-lg">
|
||||||
<CardHeader className="p-6">
|
<CardHeader className="p-6">
|
||||||
|
|||||||
+15
-31
@@ -1447,11 +1447,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Cloudinary Config
|
// Cloudinary Config
|
||||||
app.get("/api/cloudinary/config", requireAdmin, async (req, res) => {
|
app.get("/api/cloudinary/config", requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const user = req.user as User;
|
// Configuration est maintenant globale, plus de "userId"
|
||||||
const userId = user.id;
|
const config = await storage.getCloudinaryConfig();
|
||||||
const config = await storage.getCloudinaryConfig(userId);
|
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return res.json(null);
|
return res.json(null);
|
||||||
@@ -1468,19 +1467,13 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
app.post("/api/cloudinary/config", requireAdmin, async (req, res) => {
|
app.post("/api/cloudinary/config", requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const user = req.user as User;
|
// Check if config already exists globally
|
||||||
const userId = user.id;
|
const existingConfig = await storage.getCloudinaryConfig();
|
||||||
|
|
||||||
// Check if config already exists
|
|
||||||
const existingConfig = await storage.getCloudinaryConfig(userId);
|
|
||||||
|
|
||||||
let config;
|
let config;
|
||||||
if (existingConfig) {
|
if (existingConfig) {
|
||||||
// Pour les mises à jour, utiliser le schéma qui rend les secrets optionnels
|
// Pour les mises à jour, utiliser le schéma qui rend les secrets optionnels
|
||||||
const updateData = updateCloudinaryConfigSchema.parse({
|
const updateData = updateCloudinaryConfigSchema.parse(req.body);
|
||||||
...req.body,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Si apiKey/apiSecret ne sont pas fournis, garder les anciens
|
// Si apiKey/apiSecret ne sont pas fournis, garder les anciens
|
||||||
const finalData = {
|
const finalData = {
|
||||||
@@ -1489,13 +1482,10 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
apiSecret: updateData.apiSecret || existingConfig.apiSecret,
|
apiSecret: updateData.apiSecret || existingConfig.apiSecret,
|
||||||
};
|
};
|
||||||
|
|
||||||
config = await storage.updateCloudinaryConfig(userId, finalData);
|
config = await storage.updateCloudinaryConfig(finalData);
|
||||||
} else {
|
} else {
|
||||||
// Pour les créations, exiger tous les champs
|
// Pour les créations, exiger tous les champs
|
||||||
const configData = insertCloudinaryConfigSchema.parse({
|
const configData = insertCloudinaryConfigSchema.parse(req.body);
|
||||||
...req.body,
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
config = await storage.createCloudinaryConfig(configData);
|
config = await storage.createCloudinaryConfig(configData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1515,11 +1505,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
return res.status(400).json({ error: "No logo file uploaded" });
|
return res.status(400).json({ error: "No logo file uploaded" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = req.user as User;
|
// Check if Cloudinary is configured globally
|
||||||
const userId = user.id;
|
const cloudinaryConfig = await storage.getCloudinaryConfig();
|
||||||
|
|
||||||
// Check if Cloudinary is configured
|
|
||||||
const cloudinaryConfig = await storage.getCloudinaryConfig(userId);
|
|
||||||
if (!cloudinaryConfig) {
|
if (!cloudinaryConfig) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: "Cloudinary not configured. Please configure Cloudinary first."
|
error: "Cloudinary not configured. Please configure Cloudinary first."
|
||||||
@@ -1542,8 +1529,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
req.file.originalname
|
req.file.originalname
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update config with new logo public ID
|
// Update global config with new logo public ID
|
||||||
const updatedConfig = await storage.updateCloudinaryConfig(userId, {
|
const updatedConfig = await storage.updateCloudinaryConfig({
|
||||||
logoPublicId: uploadResult.publicId,
|
logoPublicId: uploadResult.publicId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1561,10 +1548,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Delete company logo
|
// Delete company logo
|
||||||
app.delete("/api/cloudinary/logo", requireAdmin, async (req, res) => {
|
app.delete("/api/cloudinary/logo", requireAdmin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const user = req.user as User;
|
const cloudinaryConfig = await storage.getCloudinaryConfig();
|
||||||
const userId = user.id;
|
|
||||||
|
|
||||||
const cloudinaryConfig = await storage.getCloudinaryConfig(userId);
|
|
||||||
if (!cloudinaryConfig || !cloudinaryConfig.logoPublicId) {
|
if (!cloudinaryConfig || !cloudinaryConfig.logoPublicId) {
|
||||||
return res.status(404).json({ error: "No logo found" });
|
return res.status(404).json({ error: "No logo found" });
|
||||||
}
|
}
|
||||||
@@ -1572,8 +1556,8 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Delete from Cloudinary
|
// Delete from Cloudinary
|
||||||
await cloudinaryService.deleteLogo(cloudinaryConfig.logoPublicId);
|
await cloudinaryService.deleteLogo(cloudinaryConfig.logoPublicId);
|
||||||
|
|
||||||
// Remove from config
|
// Remove from global config
|
||||||
await storage.updateCloudinaryConfig(userId, {
|
await storage.updateCloudinaryConfig({
|
||||||
logoPublicId: null,
|
logoPublicId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+8
-11
@@ -87,10 +87,9 @@ export interface IStorage {
|
|||||||
createAiGeneration(generation: InsertAiGeneration): Promise<AiGeneration>;
|
createAiGeneration(generation: InsertAiGeneration): Promise<AiGeneration>;
|
||||||
|
|
||||||
// Cloudinary Config
|
// Cloudinary Config
|
||||||
getCloudinaryConfig(userId: string): Promise<CloudinaryConfig | undefined>;
|
getCloudinaryConfig(): Promise<CloudinaryConfig | undefined>;
|
||||||
getAnyCloudinaryConfig(): Promise<CloudinaryConfig | undefined>;
|
|
||||||
createCloudinaryConfig(config: InsertCloudinaryConfig): Promise<CloudinaryConfig>;
|
createCloudinaryConfig(config: InsertCloudinaryConfig): Promise<CloudinaryConfig>;
|
||||||
updateCloudinaryConfig(userId: string, config: Partial<InsertCloudinaryConfig>): Promise<CloudinaryConfig>;
|
updateCloudinaryConfig(config: Partial<InsertCloudinaryConfig>): Promise<CloudinaryConfig>;
|
||||||
|
|
||||||
// OpenRouter Config
|
// OpenRouter Config
|
||||||
getOpenrouterConfig(userId: string): Promise<OpenrouterConfig | undefined>;
|
getOpenrouterConfig(userId: string): Promise<OpenrouterConfig | undefined>;
|
||||||
@@ -406,12 +405,7 @@ export class DatabaseStorage implements IStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cloudinary Config
|
// Cloudinary Config
|
||||||
async getCloudinaryConfig(userId: string): Promise<CloudinaryConfig | undefined> {
|
async getCloudinaryConfig(): Promise<CloudinaryConfig | undefined> {
|
||||||
const [config] = await db.select().from(cloudinaryConfig).where(eq(cloudinaryConfig.userId, userId));
|
|
||||||
return config || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAnyCloudinaryConfig(): Promise<CloudinaryConfig | undefined> {
|
|
||||||
const [config] = await db.select().from(cloudinaryConfig).limit(1);
|
const [config] = await db.select().from(cloudinaryConfig).limit(1);
|
||||||
return config || undefined;
|
return config || undefined;
|
||||||
}
|
}
|
||||||
@@ -421,10 +415,13 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return newConfig;
|
return newConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCloudinaryConfig(userId: string, config: Partial<InsertCloudinaryConfig>): Promise<CloudinaryConfig> {
|
async updateCloudinaryConfig(config: Partial<InsertCloudinaryConfig>): Promise<CloudinaryConfig> {
|
||||||
|
const existingConfig = await this.getCloudinaryConfig();
|
||||||
|
if (!existingConfig) throw new Error("Cloudinary config not initialized");
|
||||||
|
|
||||||
const [updated] = await db.update(cloudinaryConfig)
|
const [updated] = await db.update(cloudinaryConfig)
|
||||||
.set({ ...config, updatedAt: new Date() })
|
.set({ ...config, updatedAt: new Date() })
|
||||||
.where(eq(cloudinaryConfig.userId, userId))
|
.where(eq(cloudinaryConfig.id, existingConfig.id))
|
||||||
.returning();
|
.returning();
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-7
@@ -113,7 +113,6 @@ export type InsertUserPagePermission = z.infer<typeof insertUserPagePermissionSc
|
|||||||
|
|
||||||
export const cloudinaryConfig = pgTable("cloudinary_config", {
|
export const cloudinaryConfig = pgTable("cloudinary_config", {
|
||||||
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
id: varchar("id").primaryKey().default(sql`gen_random_uuid()`),
|
||||||
userId: varchar("user_id").notNull().unique().references(() => users.id, { onDelete: "cascade" }),
|
|
||||||
cloudName: text("cloud_name").notNull(),
|
cloudName: text("cloud_name").notNull(),
|
||||||
apiKey: text("api_key").notNull(),
|
apiKey: text("api_key").notNull(),
|
||||||
apiSecret: text("api_secret").notNull(),
|
apiSecret: text("api_secret").notNull(),
|
||||||
@@ -267,12 +266,7 @@ export const usersRelations = relations(users, ({ many, one }) => ({
|
|||||||
freesoundConfig: one(freesoundConfig),
|
freesoundConfig: one(freesoundConfig),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const cloudinaryConfigRelations = relations(cloudinaryConfig, ({ one }) => ({
|
// Pas de relation User pour CloudinaryConfig (maintenant global)
|
||||||
user: one(users, {
|
|
||||||
fields: [cloudinaryConfig.userId],
|
|
||||||
references: [users.id],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const openrouterConfigRelations = relations(openrouterConfig, ({ one }) => ({
|
export const openrouterConfigRelations = relations(openrouterConfig, ({ one }) => ({
|
||||||
user: one(users, {
|
user: one(users, {
|
||||||
|
|||||||
Reference in new issue
Block a user