fix: clear DingTalk bot credentials through explicit unbind

DingTalk bot unbind was routed through the generic adapter config update,
but undefined credential fields are dropped during JSON serialization. That
left stored Client ID and Secret values intact after clicking the unbind
button. Match the working WeChat path with a dedicated server endpoint and
frontend API call so credential removal is explicit.

Constraint: JSON request bodies cannot preserve undefined fields for generic config clearing
Rejected: Keep using updateConfig with undefined fields | serialized requests omit the fields before the server can clear them
Confidence: high
Scope-risk: narrow
Directive: Keep credential-clearing flows on explicit unbind endpoints rather than relying on partial config merges
Tested: bun test src/server/__tests__/adapters.test.ts
Tested: cd desktop && bun run test src/stores/adapterStore.test.ts
Tested: bun run check:desktop
Tested: bun run check:server
Not-tested: bun run quality:pr is blocked by repository policy requiring allow-cli-core-change approval for the broader branch impact
This commit is contained in:
程序员阿江(Relakkes) 2026-05-05 19:30:43 +08:00
parent 432a58d725
commit 71f851d60a
5 changed files with 56 additions and 7 deletions

View File

@ -41,6 +41,10 @@ export const adaptersApi = {
return api.post<AdapterFileConfig>('/api/adapters/wechat/unbind', {})
},
unbindDingtalk() {
return api.post<AdapterFileConfig>('/api/adapters/dingtalk/unbind', {})
},
beginDingtalkRegistration() {
return api.post<DingtalkRegistrationBegin>('/api/adapters/dingtalk/registration/begin', {})
},

View File

@ -7,6 +7,7 @@ describe('adapterStore IM pairing behavior', () => {
startWechatLogin: vi.fn(),
pollWechatLogin: vi.fn(),
unbindWechat: vi.fn(),
unbindDingtalk: vi.fn(),
beginDingtalkRegistration: vi.fn(),
pollDingtalkRegistration: vi.fn(),
}
@ -59,4 +60,17 @@ describe('adapterStore IM pairing behavior', () => {
expect(adaptersApi.unbindWechat).toHaveBeenCalledTimes(1)
expect(useAdapterStore.getState().config).toBe(nextConfig)
})
it('unbinds the DingTalk bot through the explicit bot action', async () => {
const nextConfig = { dingtalk: { pairedUsers: [], allowedUsers: [] } }
adaptersApi.unbindDingtalk.mockResolvedValue(nextConfig)
const { useAdapterStore } = await import('./adapterStore')
await useAdapterStore.getState().unbindDingtalkBot()
expect(adaptersApi.updateConfig).not.toHaveBeenCalled()
expect(adaptersApi.unbindDingtalk).toHaveBeenCalledTimes(1)
expect(useAdapterStore.getState().config).toBe(nextConfig)
})
})

View File

@ -132,13 +132,9 @@ export const useAdapterStore = create<AdapterStore>((set, get) => ({
},
unbindDingtalkBot: async () => {
await get().updateConfig({
dingtalk: {
clientId: undefined,
clientSecret: undefined,
pairedUsers: [],
},
})
const config = await adaptersApi.unbindDingtalk()
set({ config })
void notifyTauriRestartAdapters()
},
removePairedUser: async (platform, userId) => {

View File

@ -125,4 +125,27 @@ describe('Adapters API', () => {
expect(json.wechat.allowedUsers).toEqual([])
expect(json.wechat.pairedUsers).toEqual([])
})
it('clears DingTalk credentials on unbind', async () => {
const put = makeRequest('PUT', '/api/adapters', {
dingtalk: {
clientId: 'ding-client-1',
clientSecret: 'dingtalk-client-secret',
allowedUsers: ['ding-allowed-user'],
permissionCardTemplateId: 'permission-template',
pairedUsers: [{ userId: 'ding-user', displayName: 'DingTalk User', pairedAt: 1 }],
},
})
await handleAdaptersApi(put.req, put.url, put.segments)
const unbind = makeRequest('POST', '/api/adapters/dingtalk/unbind')
const res = await handleAdaptersApi(unbind.req, unbind.url, unbind.segments)
expect(res.status).toBe(200)
const json = await res.json() as any
expect(json.dingtalk.clientId).toBeUndefined()
expect(json.dingtalk.clientSecret).toBeUndefined()
expect(json.dingtalk.allowedUsers).toEqual([])
expect(json.dingtalk.permissionCardTemplateId).toBeUndefined()
expect(json.dingtalk.pairedUsers).toEqual([])
})
})

View File

@ -143,6 +143,18 @@ export async function handleAdaptersApi(
if (tail[0] === 'wechat') {
return handleWechatAdaptersApi(req, tail.slice(1))
}
if (tail[0] === 'dingtalk' && req.method === 'POST' && tail[1] === 'unbind') {
await adapterService.updateConfig({
dingtalk: {
clientId: undefined,
clientSecret: undefined,
allowedUsers: [],
pairedUsers: [],
permissionCardTemplateId: undefined,
},
})
return Response.json(await adapterService.getConfig())
}
if (tail[0] === 'dingtalk' && tail[1] === 'registration') {
if (req.method === 'POST' && tail[2] === 'begin') {
return Response.json(await beginDingtalkRegistration())