From 5550fb05d609c7e132003ef6c7cae5e4e5256d08 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Jul 2026 06:39:01 +0000 Subject: [PATCH] feat(openrouter): validate API key at save time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stored key that caused the persistent 401s turned out to be only 9 characters long — an accepted-but-truncated paste. Prevent this class of problem at the source: - require a minimum key length in the zod schema - verify new keys against OpenRouter's /api/v1/key endpoint on save, rejecting explicit 401/403 (network errors don't block saving) - return zod validation messages as 400 instead of a generic 500 - surface the server's error message in the settings toast (desktop and mobile) instead of a generic failure text --- client/src/pages/mobile/settings.tsx | 11 +++++++++-- client/src/pages/settings.tsx | 11 +++++++++-- server/routes.ts | 14 +++++++++++++- server/services/openrouter.ts | 16 ++++++++++++++++ shared/schema.ts | 2 +- 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/client/src/pages/mobile/settings.tsx b/client/src/pages/mobile/settings.tsx index dbaa01e..1667894 100644 --- a/client/src/pages/mobile/settings.tsx +++ b/client/src/pages/mobile/settings.tsx @@ -157,10 +157,17 @@ export default function SettingsMobile() { }); 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({ title: "Erreur", - description: "Impossible de sauvegarder la configuration OpenRouter", + description, variant: "destructive", }); }, diff --git a/client/src/pages/settings.tsx b/client/src/pages/settings.tsx index c74a96d..98275cc 100644 --- a/client/src/pages/settings.tsx +++ b/client/src/pages/settings.tsx @@ -161,10 +161,17 @@ export default function Settings() { }); 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({ title: "Erreur", - description: "Impossible de sauvegarder la configuration OpenRouter", + description, variant: "destructive", }); }, diff --git a/server/routes.ts b/server/routes.ts index 49c8163..95b9a6c 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1844,6 +1844,10 @@ export async function registerRoutes(app: Express): Promise { 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); } else { // Pour les créations, exiger tous les champs @@ -1851,14 +1855,22 @@ export async function registerRoutes(app: Express): Promise { ...req.body, 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); } // Don't send the API key back const { apiKey, ...safeConfig } = config; res.json(safeConfig); - } catch (error) { + } catch (error: any) { 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" }); } }); diff --git a/server/services/openrouter.ts b/server/services/openrouter.ts index 2137458..55e86b9 100644 --- a/server/services/openrouter.ts +++ b/server/services/openrouter.ts @@ -215,6 +215,22 @@ VERSION 3 - ÉMOTIONNELLE: 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 { + 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 { try { const response = await fetch("https://openrouter.ai/api/v1/models", { diff --git a/shared/schema.ts b/shared/schema.ts index e8ff5ad..100c817 100644 --- a/shared/schema.ts +++ b/shared/schema.ts @@ -394,7 +394,7 @@ export const insertOpenrouterConfigSchema = createInsertSchema(openrouterConfig) createdAt: true, updatedAt: true, }).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({