feat: Implement analytics, media management, and third-party service configurations with new settings pages.

This commit is contained in:
Michael committed 2026-03-04 14:25:02 +01:00
1 parent cb0ff8a0ea
commit 82befb2264
5 files changed
+354 -357

No files matched your search

+167 -156
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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, {