diff --git a/apps/backend/src/services/admin/platform-config/kuaishou-industry-service.test.ts b/apps/backend/src/services/admin/platform-config/kuaishou-industry-service.test.ts new file mode 100644 index 00000000..8e950403 --- /dev/null +++ b/apps/backend/src/services/admin/platform-config/kuaishou-industry-service.test.ts @@ -0,0 +1,80 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { isMaskedSecretValue, resolveSecretPatch } from './kuaishou-industry-service.js' +import type { KuaishouIndustrySourceConfig } from '../../platforms/kuaishou-industry/source-config-service.js' + +test('isMaskedSecretValue correctly identifies masked values and prevents overwrite', () => { + assert.equal(isMaskedSecretValue(''), true) + assert.equal(isMaskedSecretValue(' '), true) + assert.equal(isMaskedSecretValue('SK_ATD******XXrqmD'), true) + assert.equal(isMaskedSecretValue('29a8f2****79a2'), true) + assert.equal(isMaskedSecretValue('abc***xyz'), true) + assert.equal(isMaskedSecretValue('real_secret_key_12345678'), false) + assert.equal(isMaskedSecretValue('abcdef12****5678', 'abcdef12345678'), true) +}) + +test('resolveSecretPatch preserves current secrets when input is empty or masked', () => { + const current: KuaishouIndustrySourceConfig = { + enabled: true, + baseUrl: 'https://openapi.kwaixiaodian.com', + authBaseUrl: 'https://open.kwaixiaodian.com', + redirectUri: 'https://ks.khhao.com/oauth', + scopes: 'merchant_item', + authState: 'default', + appKey: '1088715114', + appSecret: 'real_kuaishou_secret_value_12345', + signSecret: 'real_sign_secret_67890', + messageSecret: 'real_message_secret_abcde', + accessToken: '', + refreshToken: '', + accessTokenExpiresAt: '', + refreshTokenExpiresAt: '', + openId: '', + grantedScopes: '', + sellerId: '', + provider: '', + platform: '', + shopId: '', + shopName: '', + version: '1', + shops: [], + lastRefreshedAt: '', + lastRefreshError: '', + } + + // 1. Payload with empty string secrets (default when page loads and user doesn't edit) + const emptyPatch = resolveSecretPatch( + { + appSecret: '', + signSecret: '', + messageSecret: '', + }, + current, + ) + assert.equal(emptyPatch.appSecret, 'real_kuaishou_secret_value_12345') + assert.equal(emptyPatch.signSecret, 'real_sign_secret_67890') + assert.equal(emptyPatch.messageSecret, 'real_message_secret_abcde') + + // 2. Payload with masked string (e.g. browser autofill of placeholder or masked token) + const maskedPatch = resolveSecretPatch( + { + appSecret: 'SK_ATD******XXrqmD', + signSecret: 'real_s****7890', + }, + current, + ) + assert.equal(maskedPatch.appSecret, 'real_kuaishou_secret_value_12345') + assert.equal(maskedPatch.signSecret, 'real_sign_secret_67890') + + // 3. Payload with genuinely new secret + const validPatch = resolveSecretPatch( + { + appSecret: 'new_valid_app_secret_value_99999', + }, + current, + ) + assert.equal(validPatch.appSecret, 'new_valid_app_secret_value_99999') + assert.equal(validPatch.signSecret, 'real_sign_secret_67890') + assert.equal(validPatch.messageSecret, 'real_message_secret_abcde') +}) diff --git a/apps/backend/src/services/admin/platform-config/kuaishou-industry-service.ts b/apps/backend/src/services/admin/platform-config/kuaishou-industry-service.ts index de25b20e..dc7239b6 100644 --- a/apps/backend/src/services/admin/platform-config/kuaishou-industry-service.ts +++ b/apps/backend/src/services/admin/platform-config/kuaishou-industry-service.ts @@ -213,14 +213,31 @@ function normalizeScopeText(value: unknown): string { .join(',') } -function resolveSecretPatch( +export function isMaskedSecretValue(value: unknown, currentSecret = ''): boolean { + const text = String(value || '').trim() + if (!text) { + return true + } + + if (text.includes('****') || text.includes('***')) { + return true + } + + if (currentSecret && text === maskSecret(currentSecret)) { + return true + } + + return false +} + +export function resolveSecretPatch( payload: JsonObject, current: KuaishouIndustrySourceConfig, ): Partial { const patch: Partial = {} for (const field of SECRET_FIELDS) { const text = String(payload[field] || '').trim() - patch[field] = text || current[field] + patch[field] = !text || isMaskedSecretValue(text, current[field]) ? current[field] : text } return patch @@ -352,7 +369,7 @@ function resolveShopSecretPatch( function readSecretString(payload: JsonObject, field: string, fallback: string): string { const text = String(payload[field] || '').trim() - return text || fallback + return !text || isMaskedSecretValue(text, fallback) ? fallback : text } function resolveAccessTokenStatus( diff --git a/apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx b/apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx index e5fa1732..ab818535 100644 --- a/apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx +++ b/apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx @@ -2,12 +2,14 @@ import { CheckCircleOutlined, CopyOutlined, DeleteOutlined, + EditOutlined, LinkOutlined, PlusOutlined, ReloadOutlined, SaveOutlined, SearchOutlined, SendOutlined, + UndoOutlined, } from '@ant-design/icons' import { Alert, @@ -1027,6 +1029,155 @@ function resolveHealthStatusColor(status: string) { return 'default' } +function SecretFieldControl({ + label, + field, + masked, + hasSecret, + value, + onChange, + placeholder, +}: { + label: string + field: string + masked?: string | undefined + hasSecret?: boolean | undefined + value: string + onChange: (value: string) => void + placeholder?: string | undefined +}) { + const [isEditing, setIsEditing] = useState(false) + const [draft, setDraft] = useState('') + + const isStaged = Boolean(value && value.trim()) + const configured = Boolean(hasSecret || (masked && masked.trim())) + + function handleStartEdit() { + setDraft('') + setIsEditing(true) + } + + function handleApply() { + const trimmed = draft.trim() + if (trimmed) { + onChange(trimmed) + setIsEditing(false) + setDraft('') + } + } + + function handleCancel() { + setDraft('') + setIsEditing(false) + } + + function handleRevert() { + onChange('') + setDraft('') + setIsEditing(false) + } + + return ( +
+
+ {label} + {isStaged ? ( + + ) : null} +
+ + {isEditing ? ( + + setDraft(e.target.value)} + onPressEnter={handleApply} + /> + + + + ) : ( +
+
+ + {isStaged + ? '已输入新密钥(待保存)' + : masked || (configured ? '••••••••••••••••' : '未配置')} + + {isStaged ? ( + + 待保存 + + ) : configured ? ( + + 已配置 + + ) : ( + 未配置 + )} +
+ +
+ )} +
+ ) +} + function KuaishouIndustryPanel({ config, onChange, @@ -1208,23 +1359,6 @@ function KuaishouIndustryPanel({ }) } - function renderSecretInput( - label: string, - field: 'appSecret' | 'signSecret' | 'messageSecret', - masked: string, - ) { - return ( -
- {label} - updateSource({ [field]: event.target.value })} - /> -
- ) - } - const shopColumns: TableColumnsType = [ { title: '启用', @@ -1446,9 +1580,30 @@ function KuaishouIndustryPanel({ value={source.version} onChange={(version) => updateSource({ version })} /> - {renderSecretInput('appSecret', 'appSecret', source.appSecretMasked)} - {renderSecretInput('signSecret', 'signSecret', source.signSecretMasked)} - {renderSecretInput('messageSecret', 'messageSecret', source.messageSecretMasked)} + updateSource({ appSecret })} + /> + updateSource({ signSecret })} + /> + updateSource({ messageSecret })} + /> @@ -1519,24 +1674,24 @@ function KuaishouIndustryPanel({ value={row.grantedScopes} onChange={(grantedScopes) => updateShop(index, { grantedScopes })} /> -
- accessToken - updateShop(index, { accessToken: event.target.value })} - /> -
-
- refreshToken - - updateShop(index, { refreshToken: event.target.value }) - } - /> -
+ updateShop(index, { accessToken })} + /> + updateShop(index, { refreshToken })} + /> updateSource({ appKey: event.target.value })} /> -
- App Secret - updateSource({ appSecret: event.target.value })} - /> -
+ updateSource({ appSecret })} + /> {label} - onChange(event.target.value)} /> + onChange(event.target.value)} + /> ) } @@ -2776,6 +2940,13 @@ function AffiliateDashPlatformPanel() { value={appSecret} onChange={(e) => setAppSecret(e.target.value)} placeholder="sk_..." + autoComplete="new-password" + data-1p-ignore="true" + data-lpignore="true" + data-bwignore="true" + data-form-type="other" + name="affiliate_dash_app_secret" + id="affiliate_dash_app_secret" /> @@ -2785,6 +2956,13 @@ function AffiliateDashPlatformPanel() { value={callbackSecret} onChange={(e) => setCallbackSecret(e.target.value)} placeholder="cb_..." + autoComplete="new-password" + data-1p-ignore="true" + data-lpignore="true" + data-bwignore="true" + data-form-type="other" + name="affiliate_dash_callback_secret" + id="affiliate_dash_callback_secret" />