Skip to content

Commit bcf918b

Browse files
authored
fix(config): prevent API calls with placeholder credentials (#3)
- 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
1 parent a6953d1 commit bcf918b

File tree

4 files changed

+82
-6
lines changed

4 files changed

+82
-6
lines changed

src/config/configUtils.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,37 @@ export interface CloudinaryEnvironment {
99
uploadPreset: string; // Default upload preset to use
1010
}
1111

12+
/**
13+
* Checks if the provided credentials are placeholder values.
14+
* @param cloudName - The cloud name to check.
15+
* @param apiKey - The API key to check.
16+
* @param apiSecret - The API secret to check.
17+
* @returns True if any of the credentials are placeholders, false otherwise.
18+
*/
19+
export function isPlaceholderConfig(
20+
cloudName: string | null,
21+
apiKey: string | null,
22+
apiSecret: string | null
23+
): boolean {
24+
const placeholderPatterns = [
25+
'your-cloud-name',
26+
'<your-api-key>',
27+
'<your-api-secret>',
28+
'<your-default-upload-preset>',
29+
'your-api-key',
30+
'your-api-secret',
31+
'your-default-upload-preset',
32+
];
33+
34+
const values = [cloudName, apiKey, apiSecret].filter(Boolean) as string[];
35+
36+
return values.some(value =>
37+
placeholderPatterns.some(pattern =>
38+
value.toLowerCase().includes(pattern.toLowerCase())
39+
)
40+
);
41+
}
42+
1243
/**
1344
* Returns the absolute path to the global Cloudinary config file.
1445
* If it doesn't exist, it creates one with a placeholder template.

src/config/detectFolderMode.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as vscode from 'vscode';
22
import { generateUserAgent } from '../utils/userAgent';
3+
import { isPlaceholderConfig } from './configUtils';
34

45
/**
56
* 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(
1819
return false;
1920
}
2021

22+
// Don't make API calls with placeholder credentials
23+
if (isPlaceholderConfig(cloudName, apiKey, apiSecret)) {
24+
return false;
25+
}
26+
2127
const authHeader = `Basic ${Buffer.from(`${apiKey}:${apiSecret}`).toString('base64')}`;
2228
const url = `https://api.cloudinary.com/v1_1/${cloudName}/folders`;
2329

src/extension.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getGlobalConfigPath,
55
loadEnvironments,
66
CloudinaryEnvironment,
7+
isPlaceholderConfig,
78
} from "./config/configUtils";
89
import detectFolderMode from "./config/detectFolderMode";
910
import { registerAllCommands } from "./commands/registerCommands";
@@ -43,6 +44,28 @@ export async function activate(context: vscode.ExtensionContext) {
4344
return;
4445
}
4546

47+
// Check if credentials are placeholder values
48+
if (isPlaceholderConfig(firstCloudName, selectedEnv.apiKey, selectedEnv.apiSecret)) {
49+
// Initialize status bar with placeholder indicator (no popup message to avoid scaring new users)
50+
statusBar = vscode.window.createStatusBarItem(
51+
vscode.StatusBarAlignment.Right,
52+
500
53+
);
54+
statusBar.text = `$(warning) Cloudinary: Not Configured`;
55+
statusBar.tooltip = "Click to configure Cloudinary credentials";
56+
statusBar.command = "cloudinary.openGlobalConfig";
57+
statusBar.show();
58+
context.subscriptions.push(statusBar);
59+
60+
// Still register the tree view but don't make API calls
61+
vscode.window.registerTreeDataProvider(
62+
"cloudinaryMediaLibrary",
63+
cloudinaryProvider
64+
);
65+
registerAllCommands(context, cloudinaryProvider, statusBar);
66+
return;
67+
}
68+
4669
cloudinaryProvider.cloudName = firstCloudName;
4770
cloudinaryProvider.apiKey = selectedEnv.apiKey;
4871
cloudinaryProvider.apiSecret = selectedEnv.apiSecret;
@@ -93,12 +116,23 @@ export async function activate(context: vscode.ExtensionContext) {
93116

94117
const env = updatedEnvs[newCloudName!];
95118

119+
// Check if updated credentials are still placeholders
120+
if (isPlaceholderConfig(newCloudName!, env.apiKey, env.apiSecret)) {
121+
statusBar.text = `$(warning) Cloudinary: Not Configured`;
122+
statusBar.tooltip = "Click to configure Cloudinary credentials";
123+
statusBar.command = "cloudinary.openGlobalConfig";
124+
// Don't show message - just update status bar silently
125+
return;
126+
}
127+
96128
cloudinaryProvider.cloudName = newCloudName;
97129
cloudinaryProvider.apiKey = env.apiKey;
98130
cloudinaryProvider.apiSecret = env.apiSecret;
99131
cloudinaryProvider.uploadPreset = env.uploadPreset;
100132

101133
statusBar.text = `$(cloud) ${newCloudName}`;
134+
statusBar.tooltip = "Click to switch Cloudinary environment";
135+
statusBar.command = "cloudinary.switchEnvironment";
102136

103137
// Update user platform for analytics
104138
(cloudinary.utils as any).userPlatform = generateUserAgent();

src/tree/treeDataProvider.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as vscode from 'vscode';
22
import { v2 as cloudinary } from 'cloudinary';
33
import CloudinaryItem from './cloudinaryItem';
44
import { handleCloudinaryError } from '../utils/cloudinaryErrorHandler';
5+
import { isPlaceholderConfig } from '../config/configUtils';
56

67
export class CloudinaryTreeDataProvider implements vscode.TreeDataProvider<CloudinaryItem> {
78
// Cloudinary credentials
@@ -53,7 +54,11 @@ export class CloudinaryTreeDataProvider implements vscode.TreeDataProvider<Cloud
5354

5455
async getChildren(element?: CloudinaryItem): Promise<CloudinaryItem[]> {
5556
if (!this.apiKey || !this.apiSecret || !this.cloudName) {
56-
vscode.window.showErrorMessage("Cloudinary credentials are not set. Please update your settings.");
57+
return [];
58+
}
59+
60+
// Prevent API calls with placeholder credentials
61+
if (isPlaceholderConfig(this.cloudName, this.apiKey, this.apiSecret)) {
5762
return [];
5863
}
5964

@@ -90,7 +95,7 @@ export class CloudinaryTreeDataProvider implements vscode.TreeDataProvider<Cloud
9095
.max_results(maxResults)
9196
.with_field(["tags", "context", "metadata"]);
9297

93-
if (nextCursor) {assetQuery.next_cursor(nextCursor);}
98+
if (nextCursor) { assetQuery.next_cursor(nextCursor); }
9499

95100
const [foldersResult, assetsResult] = await Promise.all([
96101
folderPromise,
@@ -112,8 +117,8 @@ export class CloudinaryTreeDataProvider implements vscode.TreeDataProvider<Cloud
112117
const filteredAssets = assetsResult.resources.filter((asset: any) => {
113118
const isRootLoad = folderPath === '' && !this.dynamicFolders;
114119
const isNestedAsset = asset.public_id.includes('/');
115-
if (isRootLoad && isNestedAsset) {return false;}
116-
if (this.viewState.resourceTypeFilter === 'all') {return true;}
120+
if (isRootLoad && isNestedAsset) { return false; }
121+
if (this.viewState.resourceTypeFilter === 'all') { return true; }
117122
return asset.resource_type?.toLowerCase() === this.viewState.resourceTypeFilter;
118123
});
119124

@@ -168,7 +173,7 @@ export class CloudinaryTreeDataProvider implements vscode.TreeDataProvider<Cloud
168173
.sort_by('public_id', 'asc')
169174
.max_results(maxResults);
170175

171-
if (nextCursor) {searchQuery.next_cursor(nextCursor);}
176+
if (nextCursor) { searchQuery.next_cursor(nextCursor); }
172177

173178
const assetsResult = await searchQuery.execute();
174179

@@ -245,7 +250,7 @@ export class CloudinaryTreeDataProvider implements vscode.TreeDataProvider<Cloud
245250

246251
public updateLoadMoreItem(folderPath: string, nextCursor: string) {
247252
const items = this.assetMap.get(folderPath);
248-
if (!items) {return;}
253+
if (!items) { return; }
249254

250255
const index = items.findIndex(
251256
(item) =>

0 commit comments

Comments
 (0)