From 71f851d60a07faa76fce3a5f8ab97ab5eca62a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Tue, 5 May 2026 19:30:43 +0800 Subject: [PATCH] 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 --- desktop/src/api/adapters.ts | 4 ++++ desktop/src/stores/adapterStore.test.ts | 14 ++++++++++++++ desktop/src/stores/adapterStore.ts | 10 +++------- src/server/__tests__/adapters.test.ts | 23 +++++++++++++++++++++++ src/server/api/adapters.ts | 12 ++++++++++++ 5 files changed, 56 insertions(+), 7 deletions(-) diff --git a/desktop/src/api/adapters.ts b/desktop/src/api/adapters.ts index 8855c48d..8bac289b 100644 --- a/desktop/src/api/adapters.ts +++ b/desktop/src/api/adapters.ts @@ -41,6 +41,10 @@ export const adaptersApi = { return api.post('/api/adapters/wechat/unbind', {}) }, + unbindDingtalk() { + return api.post('/api/adapters/dingtalk/unbind', {}) + }, + beginDingtalkRegistration() { return api.post('/api/adapters/dingtalk/registration/begin', {}) }, diff --git a/desktop/src/stores/adapterStore.test.ts b/desktop/src/stores/adapterStore.test.ts index 88cdf057..73ab270c 100644 --- a/desktop/src/stores/adapterStore.test.ts +++ b/desktop/src/stores/adapterStore.test.ts @@ -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) + }) }) diff --git a/desktop/src/stores/adapterStore.ts b/desktop/src/stores/adapterStore.ts index 47ff82b5..69dbd799 100644 --- a/desktop/src/stores/adapterStore.ts +++ b/desktop/src/stores/adapterStore.ts @@ -132,13 +132,9 @@ export const useAdapterStore = create((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) => { diff --git a/src/server/__tests__/adapters.test.ts b/src/server/__tests__/adapters.test.ts index 7229a7bb..fd3c7d11 100644 --- a/src/server/__tests__/adapters.test.ts +++ b/src/server/__tests__/adapters.test.ts @@ -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([]) + }) }) diff --git a/src/server/api/adapters.ts b/src/server/api/adapters.ts index 5e3a6096..c4e67cae 100644 --- a/src/server/api/adapters.ts +++ b/src/server/api/adapters.ts @@ -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())