修复快手appSecret意外修改问题

This commit is contained in:
yml2213
2026-09-13 10:42:32 +08:00
parent 1cf790d6ab
commit 447d501f7b
3 changed files with 324 additions and 49 deletions
@@ -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')
})
@@ -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<KuaishouIndustrySourceConfig> {
const patch: Partial<KuaishouIndustrySourceConfig> = {}
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(
@@ -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 (
<div style={{ minWidth: 0 }}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
minHeight: 22,
}}
>
<Typography.Text type="secondary">{label}</Typography.Text>
{isStaged ? (
<Button
type="link"
size="small"
icon={<UndoOutlined />}
style={{ padding: 0, height: 'auto', fontSize: 12 }}
onClick={handleRevert}
>
撤销修改
</Button>
) : null}
</div>
{isEditing ? (
<Space.Compact style={{ width: '100%' }}>
<Input.Password
autoFocus
autoComplete="new-password"
data-1p-ignore="true"
data-lpignore="true"
data-bwignore="true"
data-form-type="other"
name={`secure_field_${field}`}
id={`secure_field_${field}`}
placeholder={placeholder || `输入新 ${label}(留空不修改)`}
value={draft}
onChange={(e) => setDraft(e.target.value)}
onPressEnter={handleApply}
/>
<Button type="primary" onClick={handleApply} disabled={!draft.trim()}>
应用
</Button>
<Button onClick={handleCancel}>取消</Button>
</Space.Compact>
) : (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div
style={{
flex: 1,
minWidth: 0,
height: 32,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 11px',
background: '#fafafa',
border: isStaged ? '1px dashed #faad14' : '1px solid #d9d9d9',
borderRadius: 6,
fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: 12,
color: isStaged ? '#d46b08' : configured ? '#262626' : '#8c8c8c',
userSelect: 'none',
overflow: 'hidden',
}}
>
<span
style={{
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
marginRight: 8,
}}
>
{isStaged
? '已输入新密钥(待保存)'
: masked || (configured ? '••••••••••••••••' : '未配置')}
</span>
{isStaged ? (
<Tag color="warning" style={{ margin: 0, flexShrink: 0 }}>
待保存
</Tag>
) : configured ? (
<Tag color="success" style={{ margin: 0, flexShrink: 0 }}>
已配置
</Tag>
) : (
<Tag style={{ margin: 0, flexShrink: 0 }}>未配置</Tag>
)}
</div>
<Button icon={<EditOutlined />} onClick={handleStartEdit} style={{ flexShrink: 0 }}>
{configured ? '修改' : '设置'}
</Button>
</div>
)}
</div>
)
}
function KuaishouIndustryPanel({
config,
onChange,
@@ -1208,23 +1359,6 @@ function KuaishouIndustryPanel({
})
}
function renderSecretInput(
label: string,
field: 'appSecret' | 'signSecret' | 'messageSecret',
masked: string,
) {
return (
<div>
<Typography.Text type="secondary">{label}</Typography.Text>
<Input.Password
value={source[field]}
placeholder={masked || '未配置'}
onChange={(event) => updateSource({ [field]: event.target.value })}
/>
</div>
)
}
const shopColumns: TableColumnsType<AdminKuaishouIndustryShopConfig> = [
{
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)}
<SecretFieldControl
label="appSecret"
field="appSecret"
masked={source.appSecretMasked}
hasSecret={source.hasAppSecret}
value={source.appSecret}
onChange={(appSecret) => updateSource({ appSecret })}
/>
<SecretFieldControl
label="signSecret"
field="signSecret"
masked={source.signSecretMasked}
hasSecret={source.hasSignSecret}
value={source.signSecret}
onChange={(signSecret) => updateSource({ signSecret })}
/>
<SecretFieldControl
label="messageSecret"
field="messageSecret"
masked={source.messageSecretMasked}
hasSecret={source.hasMessageSecret}
value={source.messageSecret}
onChange={(messageSecret) => updateSource({ messageSecret })}
/>
</div>
</Card>
@@ -1519,24 +1674,24 @@ function KuaishouIndustryPanel({
value={row.grantedScopes}
onChange={(grantedScopes) => updateShop(index, { grantedScopes })}
/>
<div>
<Typography.Text type="secondary">accessToken</Typography.Text>
<Input.Password
value={row.accessToken}
placeholder={row.accessTokenMasked || '留空保持原值'}
onChange={(event) => updateShop(index, { accessToken: event.target.value })}
/>
</div>
<div>
<Typography.Text type="secondary">refreshToken</Typography.Text>
<Input.Password
value={row.refreshToken}
placeholder={row.refreshTokenMasked || '留空保持原值'}
onChange={(event) =>
updateShop(index, { refreshToken: event.target.value })
}
/>
</div>
<SecretFieldControl
label="accessToken"
field={`shop_${index}_accessToken`}
masked={row.accessTokenMasked}
hasSecret={row.hasAccessToken}
value={row.accessToken}
placeholder="输入新 accessToken(留空保持原值)"
onChange={(accessToken) => updateShop(index, { accessToken })}
/>
<SecretFieldControl
label="refreshToken"
field={`shop_${index}_refreshToken`}
masked={row.refreshTokenMasked}
hasSecret={row.hasRefreshToken}
value={row.refreshToken}
placeholder="输入新 refreshToken(留空保持原值)"
onChange={(refreshToken) => updateShop(index, { refreshToken })}
/>
<LabeledInput
label="accessToken 过期时间"
value={row.accessTokenExpiresAt}
@@ -1696,13 +1851,14 @@ function KuaishouFeifeiPlatformPanel({
onChange={(event) => updateSource({ appKey: event.target.value })}
/>
</div>
<div>
<Typography.Text type="secondary">App Secret</Typography.Text>
<Input.Password
value={source.appSecret}
onChange={(event) => updateSource({ appSecret: event.target.value })}
/>
</div>
<SecretFieldControl
label="App Secret"
field="feifei_appSecret"
masked={config.effective.hasAppSecret ? '••••••••••••••••' : ''}
hasSecret={config.effective.hasAppSecret}
value={source.appSecret}
onChange={(appSecret) => updateSource({ appSecret })}
/>
<NumberField
label="超时毫秒"
value={source.timeoutMs}
@@ -2545,7 +2701,15 @@ function LabeledInput({
return (
<div>
<Typography.Text type="secondary">{label}</Typography.Text>
<InputComponent value={value} onChange={(event) => onChange(event.target.value)} />
<InputComponent
autoComplete={password ? 'new-password' : 'off'}
data-1p-ignore="true"
data-lpignore="true"
data-bwignore="true"
data-form-type="other"
value={value}
onChange={(event) => onChange(event.target.value)}
/>
</div>
)
}
@@ -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"
/>
</Space>
<Space wrap>
@@ -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"
/>
</Space>
<Space wrap>