From 82befb22641e0498cb6bdacd70f42d295b5e24a9 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Wed, 4 Mar 2026 14:25:02 +0100 Subject: [PATCH] feat: Implement analytics, media management, and third-party service configurations with new settings pages. --- client/src/pages/mobile/settings.tsx | 323 ++++++++++++++------------- client/src/pages/settings.tsx | 315 +++++++++++++------------- server/routes.ts | 46 ++-- server/storage.ts | 19 +- shared/schema.ts | 8 +- 5 files changed, 354 insertions(+), 357 deletions(-) diff --git a/client/src/pages/mobile/settings.tsx b/client/src/pages/mobile/settings.tsx index f629659..6ad7730 100644 --- a/client/src/pages/mobile/settings.tsx +++ b/client/src/pages/mobile/settings.tsx @@ -27,6 +27,13 @@ export default function SettingsMobile() { const [logoPreview, setLogoPreview] = useState(null); 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({ queryKey: ['/api/cloudinary/config'], }); @@ -355,169 +362,173 @@ export default function SettingsMobile() { {/* Cloudinary */} - - - - - Configuration Cloudinary - - - Configurez Cloudinary pour le stockage d'images dans le cloud - - - -
- - setCloudName(e.target.value)} - placeholder="mon-cloud-name" - className="min-h-[48px]" - data-testid="input-cloud-name" - /> -

- Trouvez-le dans votre tableau de bord Cloudinary -

-
-
- - setApiKey(e.target.value)} - placeholder="123456789012345" - className="min-h-[48px]" - data-testid="input-api-key" - /> -
-
- - setApiSecret(e.target.value)} - placeholder="••••••••••••••••" - className="min-h-[48px]" - data-testid="input-api-secret" - /> -

- Votre API Secret restera sécurisé et ne sera jamais exposé -

-
- -
-
+ {isAdmin && ( + + + + + Configuration Cloudinary + + + Configurez Cloudinary pour le stockage d'images dans le cloud + + + +
+ + setCloudName(e.target.value)} + placeholder="mon-cloud-name" + className="min-h-[48px]" + data-testid="input-cloud-name" + /> +

+ Trouvez-le dans votre tableau de bord Cloudinary +

+
+
+ + setApiKey(e.target.value)} + placeholder="123456789012345" + className="min-h-[48px]" + data-testid="input-api-key" + /> +
+
+ + setApiSecret(e.target.value)} + placeholder="••••••••••••••••" + className="min-h-[48px]" + data-testid="input-api-secret" + /> +

+ Votre API Secret restera sécurisé et ne sera jamais exposé +

+
+ +
+
+ )} {/* Logo */} - - - - - Logo de l'entreprise - - - Uploadez votre logo pour l'ajouter aux images éditées - - - - {logoPreview ? ( -
-
- Logo - -
-
-
- ) : ( -
-
- -

- Aucun logo uploadé. Choisissez une image PNG ou JPG. -

- - { - handleLogoChange(e); - if (e.target.files?.[0]) { - uploadLogoMutation.mutate(e.target.files[0]); - } - }} - /> + ) : ( +
+
+ +

+ Aucun logo uploadé. Choisissez une image PNG ou JPG. +

+ + { + handleLogoChange(e); + if (e.target.files?.[0]) { + uploadLogoMutation.mutate(e.target.files[0]); + } + }} + /> +
+ {!cloudinaryConfig && ( +

+ Veuillez d'abord configurer Cloudinary ci-dessus +

+ )}
- {!cloudinaryConfig && ( -

- Veuillez d'abord configurer Cloudinary ci-dessus -

- )} -
- )} - - + )} + + + )}
diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx index 3392345..0cb87e6 100644 --- a/client/src/pages/settings.tsx +++ b/client/src/pages/settings.tsx @@ -29,6 +29,13 @@ export default function Settings() { const [ffmpegApiKey, setFfmpegApiKey] = useState(""); 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({ queryKey: ['/api/cloudinary/config'], }); @@ -448,66 +455,68 @@ export default function Settings() {
- - - - - Configuration Cloudinary - - - Configurez Cloudinary pour le stockage d'images dans le cloud - - - -
- - setCloudName(e.target.value)} - placeholder="mon-cloud-name" - data-testid="input-cloud-name" - /> -

- Trouvez-le dans votre tableau de bord Cloudinary -

-
-
- - setApiKey(e.target.value)} - placeholder="123456789012345" - data-testid="input-api-key" - /> -
-
- - setApiSecret(e.target.value)} - placeholder="••••••••••••••••" - data-testid="input-api-secret" - /> -

- Votre API Secret restera sécurisé et ne sera jamais exposé -

-
- -
-
+ {isAdmin && ( + + + + + Configuration Cloudinary + + + Configurez Cloudinary pour le stockage d'images dans le cloud + + + +
+ + setCloudName(e.target.value)} + placeholder="mon-cloud-name" + data-testid="input-cloud-name" + /> +

+ Trouvez-le dans votre tableau de bord Cloudinary +

+
+
+ + setApiKey(e.target.value)} + placeholder="123456789012345" + data-testid="input-api-key" + /> +
+
+ + setApiSecret(e.target.value)} + placeholder="••••••••••••••••" + data-testid="input-api-secret" + /> +

+ Votre API Secret restera sécurisé et ne sera jamais exposé +

+
+ +
+
+ )} @@ -623,103 +632,105 @@ export default function Settings() { - - - - - Logo de l'entreprise - - - Uploadez votre logo pour l'ajouter aux images éditées - - - - {logoPreview ? ( -
-
- Logo - -
-
-
- ) : ( -
-
- -

- Aucun logo uploadé. Choisissez une image PNG ou JPG. -

- - { - handleLogoChange(e); - if (e.target.files?.[0]) { - uploadLogoMutation.mutate(e.target.files[0]); - } - }} - /> + ) : ( +
+
+ +

+ Aucun logo uploadé. Choisissez une image PNG ou JPG. +

+ + { + handleLogoChange(e); + if (e.target.files?.[0]) { + uploadLogoMutation.mutate(e.target.files[0]); + } + }} + /> +
+ {!cloudinaryConfig && ( +

+ Veuillez d'abord configurer Cloudinary ci-dessus +

+ )}
- {!cloudinaryConfig && ( -

- Veuillez d'abord configurer Cloudinary ci-dessus -

- )} -
- )} - - + )} + + + )} diff --git a/server/routes.ts b/server/routes.ts index 315fdb2..f8ef1aa 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1447,11 +1447,10 @@ export async function registerRoutes(app: Express): Promise { }); // Cloudinary Config - app.get("/api/cloudinary/config", requireAdmin, async (req, res) => { + app.get("/api/cloudinary/config", requireAuth, async (req, res) => { try { - const user = req.user as User; - const userId = user.id; - const config = await storage.getCloudinaryConfig(userId); + // Configuration est maintenant globale, plus de "userId" + const config = await storage.getCloudinaryConfig(); if (!config) { return res.json(null); @@ -1468,19 +1467,13 @@ export async function registerRoutes(app: Express): Promise { app.post("/api/cloudinary/config", requireAdmin, async (req, res) => { try { - const user = req.user as User; - const userId = user.id; - - // Check if config already exists - const existingConfig = await storage.getCloudinaryConfig(userId); + // Check if config already exists globally + const existingConfig = await storage.getCloudinaryConfig(); let config; if (existingConfig) { // Pour les mises à jour, utiliser le schéma qui rend les secrets optionnels - const updateData = updateCloudinaryConfigSchema.parse({ - ...req.body, - userId, - }); + const updateData = updateCloudinaryConfigSchema.parse(req.body); // Si apiKey/apiSecret ne sont pas fournis, garder les anciens const finalData = { @@ -1489,13 +1482,10 @@ export async function registerRoutes(app: Express): Promise { apiSecret: updateData.apiSecret || existingConfig.apiSecret, }; - config = await storage.updateCloudinaryConfig(userId, finalData); + config = await storage.updateCloudinaryConfig(finalData); } else { // Pour les créations, exiger tous les champs - const configData = insertCloudinaryConfigSchema.parse({ - ...req.body, - userId, - }); + const configData = insertCloudinaryConfigSchema.parse(req.body); config = await storage.createCloudinaryConfig(configData); } @@ -1515,11 +1505,8 @@ export async function registerRoutes(app: Express): Promise { return res.status(400).json({ error: "No logo file uploaded" }); } - const user = req.user as User; - const userId = user.id; - - // Check if Cloudinary is configured - const cloudinaryConfig = await storage.getCloudinaryConfig(userId); + // Check if Cloudinary is configured globally + const cloudinaryConfig = await storage.getCloudinaryConfig(); if (!cloudinaryConfig) { return res.status(400).json({ error: "Cloudinary not configured. Please configure Cloudinary first." @@ -1542,8 +1529,8 @@ export async function registerRoutes(app: Express): Promise { req.file.originalname ); - // Update config with new logo public ID - const updatedConfig = await storage.updateCloudinaryConfig(userId, { + // Update global config with new logo public ID + const updatedConfig = await storage.updateCloudinaryConfig({ logoPublicId: uploadResult.publicId, }); @@ -1561,10 +1548,7 @@ export async function registerRoutes(app: Express): Promise { // Delete company logo app.delete("/api/cloudinary/logo", requireAdmin, async (req, res) => { try { - const user = req.user as User; - const userId = user.id; - - const cloudinaryConfig = await storage.getCloudinaryConfig(userId); + const cloudinaryConfig = await storage.getCloudinaryConfig(); if (!cloudinaryConfig || !cloudinaryConfig.logoPublicId) { return res.status(404).json({ error: "No logo found" }); } @@ -1572,8 +1556,8 @@ export async function registerRoutes(app: Express): Promise { // Delete from Cloudinary await cloudinaryService.deleteLogo(cloudinaryConfig.logoPublicId); - // Remove from config - await storage.updateCloudinaryConfig(userId, { + // Remove from global config + await storage.updateCloudinaryConfig({ logoPublicId: null, }); diff --git a/server/storage.ts b/server/storage.ts index 9a6f2e6..9fbba46 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -87,10 +87,9 @@ export interface IStorage { createAiGeneration(generation: InsertAiGeneration): Promise; // Cloudinary Config - getCloudinaryConfig(userId: string): Promise; - getAnyCloudinaryConfig(): Promise; + getCloudinaryConfig(): Promise; createCloudinaryConfig(config: InsertCloudinaryConfig): Promise; - updateCloudinaryConfig(userId: string, config: Partial): Promise; + updateCloudinaryConfig(config: Partial): Promise; // OpenRouter Config getOpenrouterConfig(userId: string): Promise; @@ -406,12 +405,7 @@ export class DatabaseStorage implements IStorage { } // Cloudinary Config - async getCloudinaryConfig(userId: string): Promise { - const [config] = await db.select().from(cloudinaryConfig).where(eq(cloudinaryConfig.userId, userId)); - return config || undefined; - } - - async getAnyCloudinaryConfig(): Promise { + async getCloudinaryConfig(): Promise { const [config] = await db.select().from(cloudinaryConfig).limit(1); return config || undefined; } @@ -421,10 +415,13 @@ export class DatabaseStorage implements IStorage { return newConfig; } - async updateCloudinaryConfig(userId: string, config: Partial): Promise { + async updateCloudinaryConfig(config: Partial): Promise { + const existingConfig = await this.getCloudinaryConfig(); + if (!existingConfig) throw new Error("Cloudinary config not initialized"); + const [updated] = await db.update(cloudinaryConfig) .set({ ...config, updatedAt: new Date() }) - .where(eq(cloudinaryConfig.userId, userId)) + .where(eq(cloudinaryConfig.id, existingConfig.id)) .returning(); return updated; } diff --git a/shared/schema.ts b/shared/schema.ts index 8561737..724fc69 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -113,7 +113,6 @@ export type InsertUserPagePermission = z.infer users.id, { onDelete: "cascade" }), cloudName: text("cloud_name").notNull(), apiKey: text("api_key").notNull(), apiSecret: text("api_secret").notNull(), @@ -267,12 +266,7 @@ export const usersRelations = relations(users, ({ many, one }) => ({ freesoundConfig: one(freesoundConfig), })); -export const cloudinaryConfigRelations = relations(cloudinaryConfig, ({ one }) => ({ - user: one(users, { - fields: [cloudinaryConfig.userId], - references: [users.id], - }), -})); +// Pas de relation User pour CloudinaryConfig (maintenant global) export const openrouterConfigRelations = relations(openrouterConfig, ({ one }) => ({ user: one(users, {