Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 54 additions & 20 deletions server/src/routes/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,35 @@ import { config } from '../config.js';

const router = Router();

type SecretStatus = 'ok' | 'empty' | 'decrypt_failed';

function getMaskedSecretResult(params: {
encryptedValue: unknown;
encryptionKey: string;
kind: 'AI API key' | 'WebDAV password' | 'GitHub token';
configId?: unknown;
configName?: unknown;
}): { decryptedValue: string; status: SecretStatus } {
const { encryptedValue, encryptionKey, kind, configId, configName } = params;

if (!encryptedValue || typeof encryptedValue !== 'string') {
return { decryptedValue: '', status: 'empty' };
}

try {
return {
decryptedValue: decrypt(encryptedValue, encryptionKey),
status: 'ok',
};
} catch (error) {
const detail = [configId ? `id=${String(configId)}` : '', configName ? `name=${String(configName)}` : '']
.filter(Boolean)
.join(', ');
console.warn(`[configs] Failed to decrypt ${kind}${detail ? ` (${detail})` : ''}:`, error);
return { decryptedValue: '', status: 'decrypt_failed' };
}
}

// ── AI Configs ──

function maskApiKey(key: string | null | undefined): string {
Expand All @@ -20,19 +49,21 @@ router.get('/api/configs/ai', (req, res) => {
const shouldDecrypt = req.query.decrypt === 'true';
const rows = db.prepare('SELECT * FROM ai_configs ORDER BY id ASC').all() as Record<string, unknown>[];
const configs = rows.map((row) => {
let decryptedKey = '';
try {
if (row.api_key_encrypted && typeof row.api_key_encrypted === 'string') {
decryptedKey = decrypt(row.api_key_encrypted, config.encryptionKey);
}
} catch { /* leave empty */ }
const { decryptedValue, status } = getMaskedSecretResult({
encryptedValue: row.api_key_encrypted,
encryptionKey: config.encryptionKey,
kind: 'AI API key',
configId: row.id,
configName: row.name,
});
return {
id: row.id,
name: row.name,
apiType: row.api_type,
model: row.model,
baseUrl: row.base_url,
apiKey: shouldDecrypt ? decryptedKey : maskApiKey(decryptedKey),
apiKey: shouldDecrypt ? decryptedValue : maskApiKey(decryptedValue),
apiKeyStatus: status,
isActive: !!row.is_active,
customPrompt: row.custom_prompt ?? null,
useCustomPrompt: !!row.use_custom_prompt,
Expand Down Expand Up @@ -199,18 +230,20 @@ router.get('/api/configs/webdav', (req, res) => {
const shouldDecrypt = req.query.decrypt === 'true';
const rows = db.prepare('SELECT * FROM webdav_configs ORDER BY id ASC').all() as Record<string, unknown>[];
const configs = rows.map((row) => {
let decryptedPwd = '';
try {
if (row.password_encrypted && typeof row.password_encrypted === 'string') {
decryptedPwd = decrypt(row.password_encrypted, config.encryptionKey);
}
} catch { /* leave empty */ }
const { decryptedValue, status } = getMaskedSecretResult({
encryptedValue: row.password_encrypted,
encryptionKey: config.encryptionKey,
kind: 'WebDAV password',
configId: row.id,
configName: row.name,
});
return {
id: row.id,
name: row.name,
url: row.url,
username: row.username,
password: shouldDecrypt ? decryptedPwd : maskPassword(decryptedPwd),
password: shouldDecrypt ? decryptedValue : maskPassword(decryptedValue),
passwordStatus: status,
path: row.path,
isActive: !!row.is_active,
};
Expand Down Expand Up @@ -367,12 +400,13 @@ router.get('/api/settings', (_req, res) => {
let value = row.value as string | null;

if (key === 'github_token' && value) {
try {
const decrypted = decrypt(value, config.encryptionKey);
value = maskApiKey(decrypted);
} catch {
value = '****';
}
const { decryptedValue, status } = getMaskedSecretResult({
encryptedValue: value,
encryptionKey: config.encryptionKey,
kind: 'GitHub token',
});
value = status === 'empty' ? '' : maskApiKey(decryptedValue);
settings.github_token_status = status;
}
Comment on lines 402 to 410
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Return github_token_status even when token is empty.

Line 402 currently skips status assignment when value is empty/null, so consumers can’t distinguish “missing status” vs explicit empty state. Please run the helper for every github_token row and let it return 'empty'.

💡 Suggested fix
-      if (key === 'github_token' && value) {
+      if (key === 'github_token') {
         const { decryptedValue, status } = getMaskedSecretResult({
           encryptedValue: value,
           encryptionKey: config.encryptionKey,
           kind: 'GitHub token',
         });
         value = status === 'empty' ? '' : maskApiKey(decryptedValue);
         settings.github_token_status = status;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/src/routes/configs.ts` around lines 402 - 410, The github_token branch
currently only runs getMaskedSecretResult when value is truthy, so
settings.github_token_status is left undefined for empty tokens; always call
getMaskedSecretResult for key === 'github_token' (passing encryptedValue: value
and encryptionKey: config.encryptionKey), set settings.github_token_status =
status unconditionally, and then set value = status === 'empty' ? '' :
maskApiKey(decryptedValue) so consumers can distinguish an explicit empty token
from a missing status (update logic around getMaskedSecretResult, maskApiKey,
and settings.github_token_status accordingly).


settings[key] = value;
Expand Down
16 changes: 16 additions & 0 deletions src/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,14 @@ Focus on practicality and accurate categorization to help users quickly understa
{(config.apiType || 'openai').toUpperCase()} • {config.baseUrl} • {config.model} • {t('并发数', 'Concurrency')}: {config.concurrency || 1}
{config.reasoningEffort ? ` • reasoning: ${config.reasoningEffort}` : ''}
</p>
{config.apiKeyStatus === 'decrypt_failed' && (
<p className="mt-1 text-sm text-amber-600 dark:text-amber-400">
{t(
'存储的 API Key 无法解密,请重新输入并保存该配置。',
'The stored API key could not be decrypted. Please re-enter and save this configuration.'
)}
</p>
)}
Comment on lines +946 to +953
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add equivalent warning flow for github_token_status.

You now surface AI/WebDAV decrypt failures, but GitHub token decrypt failures still have no visible warning in the settings UI. That leaves one credential type silent from the user perspective.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/SettingsPanel.tsx` around lines 946 - 953, Add the same
visible warning flow for GitHub token decrypt failures by rendering a warning
paragraph when config.github_token_status === 'decrypt_failed' next to the
existing API key block in SettingsPanel (where config.apiKeyStatus is checked);
mirror the styling and translation usage (className "mt-1 text-sm text-amber-600
dark:text-amber-400" and t(...) pattern) and provide a bilingual message
analogous to the API key one (e.g., "存储的 GitHub Token 无法解密,请重新输入并保存该配置。" / "The
stored GitHub token could not be decrypted. Please re-enter and save this
configuration.") so users see the same warning for github_token_status decrypt
failures.

</div>
</div>

Expand Down Expand Up @@ -1135,6 +1143,14 @@ Focus on practicality and accurate categorization to help users quickly understa
<p className="text-sm text-gray-500 dark:text-gray-400">
{config.url} • {config.path}
</p>
{config.passwordStatus === 'decrypt_failed' && (
<p className="mt-1 text-sm text-amber-600 dark:text-amber-400">
{t(
'存储的 WebDAV 密码无法解密,请重新输入并保存该配置。',
'The stored WebDAV password could not be decrypted. Please re-enter and save this configuration.'
)}
</p>
)}
</div>
</div>

Expand Down
4 changes: 4 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export interface GitHubUser {
export type AIApiType = 'openai' | 'openai-responses' | 'claude' | 'gemini';
export type AIReasoningEffort = 'none' | 'low' | 'medium' | 'high' | 'xhigh';

export type SecretStatus = 'ok' | 'empty' | 'decrypt_failed';

export interface AIConfig {
id: string;
name: string;
Expand All @@ -82,6 +84,7 @@ export interface AIConfig {
useCustomPrompt?: boolean; // 是否使用自定义提示词
concurrency?: number; // AI分析并发数,默认为1
reasoningEffort?: AIReasoningEffort; // OpenAI GPT-5/Responses 可选 reasoning 强度
apiKeyStatus?: SecretStatus;
}

export interface WebDAVConfig {
Expand All @@ -92,6 +95,7 @@ export interface WebDAVConfig {
password: string;
path: string;
isActive: boolean;
passwordStatus?: SecretStatus;
}

export interface SearchFilters {
Expand Down
Loading