From 7912f3fd13f2c0016f5737820a86e97d9176a794 Mon Sep 17 00:00:00 2001 From: Nick Bradley Date: Wed, 12 Nov 2025 15:21:29 +0000 Subject: [PATCH] fix(config): prevent API calls with placeholder credentials - Added isPlaceholderConfig() helper to detect placeholder values - Status bar displays 'Not Configured' state when placeholders detected - Tree view remains empty (no scary messages) with placeholder credentials - Config file watcher validates credentials before making API calls - Prevents unnecessary errors in logs from invalid API requests - Provides friendly first-time user experience without alarming popups --- src/config/configUtils.ts | 31 +++++++++++++++++++++++++++++++ src/config/detectFolderMode.ts | 6 ++++++ src/extension.ts | 34 ++++++++++++++++++++++++++++++++++ src/tree/treeDataProvider.ts | 17 +++++++++++------ 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/config/configUtils.ts b/src/config/configUtils.ts index 1b31ef1..c47cadd 100644 --- a/src/config/configUtils.ts +++ b/src/config/configUtils.ts @@ -9,6 +9,37 @@ export interface CloudinaryEnvironment { uploadPreset: string; // Default upload preset to use } +/** + * Checks if the provided credentials are placeholder values. + * @param cloudName - The cloud name to check. + * @param apiKey - The API key to check. + * @param apiSecret - The API secret to check. + * @returns True if any of the credentials are placeholders, false otherwise. + */ +export function isPlaceholderConfig( + cloudName: string | null, + apiKey: string | null, + apiSecret: string | null +): boolean { + const placeholderPatterns = [ + 'your-cloud-name', + '', + '', + '', + 'your-api-key', + 'your-api-secret', + 'your-default-upload-preset', + ]; + + const values = [cloudName, apiKey, apiSecret].filter(Boolean) as string[]; + + return values.some(value => + placeholderPatterns.some(pattern => + value.toLowerCase().includes(pattern.toLowerCase()) + ) + ); +} + /** * Returns the absolute path to the global Cloudinary config file. * If it doesn't exist, it creates one with a placeholder template. diff --git a/src/config/detectFolderMode.ts b/src/config/detectFolderMode.ts index 7396677..5537b3d 100644 --- a/src/config/detectFolderMode.ts +++ b/src/config/detectFolderMode.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import { generateUserAgent } from '../utils/userAgent'; +import { isPlaceholderConfig } from './configUtils'; /** * Detects if the cloud supports dynamic folders by making a request to the root folder API. @@ -18,6 +19,11 @@ export default async function detectFolderMode( return false; } + // Don't make API calls with placeholder credentials + if (isPlaceholderConfig(cloudName, apiKey, apiSecret)) { + return false; + } + const authHeader = `Basic ${Buffer.from(`${apiKey}:${apiSecret}`).toString('base64')}`; const url = `https://api.cloudinary.com/v1_1/${cloudName}/folders`; diff --git a/src/extension.ts b/src/extension.ts index b44dc70..44b480b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,6 +4,7 @@ import { getGlobalConfigPath, loadEnvironments, CloudinaryEnvironment, + isPlaceholderConfig, } from "./config/configUtils"; import detectFolderMode from "./config/detectFolderMode"; import { registerAllCommands } from "./commands/registerCommands"; @@ -43,6 +44,28 @@ export async function activate(context: vscode.ExtensionContext) { return; } + // Check if credentials are placeholder values + if (isPlaceholderConfig(firstCloudName, selectedEnv.apiKey, selectedEnv.apiSecret)) { + // Initialize status bar with placeholder indicator (no popup message to avoid scaring new users) + statusBar = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Right, + 500 + ); + statusBar.text = `$(warning) Cloudinary: Not Configured`; + statusBar.tooltip = "Click to configure Cloudinary credentials"; + statusBar.command = "cloudinary.openGlobalConfig"; + statusBar.show(); + context.subscriptions.push(statusBar); + + // Still register the tree view but don't make API calls + vscode.window.registerTreeDataProvider( + "cloudinaryMediaLibrary", + cloudinaryProvider + ); + registerAllCommands(context, cloudinaryProvider, statusBar); + return; + } + cloudinaryProvider.cloudName = firstCloudName; cloudinaryProvider.apiKey = selectedEnv.apiKey; cloudinaryProvider.apiSecret = selectedEnv.apiSecret; @@ -93,12 +116,23 @@ export async function activate(context: vscode.ExtensionContext) { const env = updatedEnvs[newCloudName!]; + // Check if updated credentials are still placeholders + if (isPlaceholderConfig(newCloudName!, env.apiKey, env.apiSecret)) { + statusBar.text = `$(warning) Cloudinary: Not Configured`; + statusBar.tooltip = "Click to configure Cloudinary credentials"; + statusBar.command = "cloudinary.openGlobalConfig"; + // Don't show message - just update status bar silently + return; + } + cloudinaryProvider.cloudName = newCloudName; cloudinaryProvider.apiKey = env.apiKey; cloudinaryProvider.apiSecret = env.apiSecret; cloudinaryProvider.uploadPreset = env.uploadPreset; statusBar.text = `$(cloud) ${newCloudName}`; + statusBar.tooltip = "Click to switch Cloudinary environment"; + statusBar.command = "cloudinary.switchEnvironment"; // Update user platform for analytics (cloudinary.utils as any).userPlatform = generateUserAgent(); diff --git a/src/tree/treeDataProvider.ts b/src/tree/treeDataProvider.ts index 3f10511..70c5374 100644 --- a/src/tree/treeDataProvider.ts +++ b/src/tree/treeDataProvider.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { v2 as cloudinary } from 'cloudinary'; import CloudinaryItem from './cloudinaryItem'; import { handleCloudinaryError } from '../utils/cloudinaryErrorHandler'; +import { isPlaceholderConfig } from '../config/configUtils'; export class CloudinaryTreeDataProvider implements vscode.TreeDataProvider { // Cloudinary credentials @@ -53,7 +54,11 @@ export class CloudinaryTreeDataProvider implements vscode.TreeDataProvider { if (!this.apiKey || !this.apiSecret || !this.cloudName) { - vscode.window.showErrorMessage("Cloudinary credentials are not set. Please update your settings."); + return []; + } + + // Prevent API calls with placeholder credentials + if (isPlaceholderConfig(this.cloudName, this.apiKey, this.apiSecret)) { return []; } @@ -90,7 +95,7 @@ export class CloudinaryTreeDataProvider implements vscode.TreeDataProvider { const isRootLoad = folderPath === '' && !this.dynamicFolders; const isNestedAsset = asset.public_id.includes('/'); - if (isRootLoad && isNestedAsset) {return false;} - if (this.viewState.resourceTypeFilter === 'all') {return true;} + if (isRootLoad && isNestedAsset) { return false; } + if (this.viewState.resourceTypeFilter === 'all') { return true; } return asset.resource_type?.toLowerCase() === this.viewState.resourceTypeFilter; }); @@ -168,7 +173,7 @@ export class CloudinaryTreeDataProvider implements vscode.TreeDataProvider