From 0ff78707d1b84fd0e69ecb7b823306e2a9704942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20P=C3=B6ttker?= Date: Mon, 28 Sep 2026 09:53:47 +0200 Subject: [PATCH] =?UTF-8?q?feat(agrarmonitor):=20Cookies=20in=20den=20Eins?= =?UTF-8?q?tellungen=20zur=C3=BCcksetzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neuer Button „Cookies zurücksetzen“ neben „Status abrufen“. Er löscht die gespeicherte Agrarmonitor-Session samt Cookie-Datei; beim nächsten Zugriff meldet sich der Manager neu an. Eine laufende Connector-Instanz wird über clearSession() zurückgesetzt und verworfen, damit sie ihren alten Cookie-Jar nicht wieder in die Datei schreibt. Während einer Lieferantennummern-Vergabe ist das Zurücksetzen gesperrt. Co-Authored-By: Claude Opus 5.5 --- .../agrarmonitor/agrarmonitor.controller.ts | 13 +++++ .../agrarmonitor/agrarmonitor.service.spec.ts | 44 ++++++++++++++++ .../src/agrarmonitor/agrarmonitor.service.ts | 51 +++++++++++++------ paperless-frontend/src/api/settings.ts | 2 + paperless-frontend/src/pages/SettingsPage.tsx | 41 +++++++++++++-- 5 files changed, 133 insertions(+), 18 deletions(-) create mode 100644 paperless-backend/src/agrarmonitor/agrarmonitor.service.spec.ts diff --git a/paperless-backend/src/agrarmonitor/agrarmonitor.controller.ts b/paperless-backend/src/agrarmonitor/agrarmonitor.controller.ts index 7cfd23e..413735b 100644 --- a/paperless-backend/src/agrarmonitor/agrarmonitor.controller.ts +++ b/paperless-backend/src/agrarmonitor/agrarmonitor.controller.ts @@ -1,6 +1,7 @@ import { BadRequestException, Body, + ConflictException, Controller, Get, HttpCode, @@ -36,6 +37,18 @@ export class AgrarmonitorController { return this.service.registerDevice(body.pcName, body.agrarmonitorId); } + @Post('reset-cookies') + @HttpCode(200) + @RequirePermissions(Permission.MANAGE_SETTINGS) + async resetCookies() { + if (this.lieferantennummernService.getStatus().aktiv) { + throw new ConflictException( + 'Während der Lieferantennummern-Vergabe können die Cookies nicht gelöscht werden.', + ); + } + return this.service.resetCookies(); + } + @Get('polling-config') @RequirePermissions(Permission.MANAGE_SETTINGS) async getPollingConfig() { diff --git a/paperless-backend/src/agrarmonitor/agrarmonitor.service.spec.ts b/paperless-backend/src/agrarmonitor/agrarmonitor.service.spec.ts new file mode 100644 index 0000000..c45cf33 --- /dev/null +++ b/paperless-backend/src/agrarmonitor/agrarmonitor.service.spec.ts @@ -0,0 +1,44 @@ +import type { ConfigService } from '@nestjs/config'; +import type { AgrarmonitorConnectorResult } from 'agrarmonitor-connector'; +import { AgrarmonitorService } from './agrarmonitor.service'; + +const storeClear = jest.fn(() => Promise.resolve()); + +// Der echte Connector lädt jsdom (ESM), das Jest nicht verarbeiten kann. +jest.mock('agrarmonitor-connector', () => ({ + FileCookieStore: jest.fn(() => ({ clear: storeClear })), + AesGcmCookieEncryptor: jest.fn(), + createAgrarmonitorClient: jest.fn(), +})); + +function service(): AgrarmonitorService { + const config = { + get: (_key: string, fallback?: string) => fallback, + } as unknown as ConfigService; + return new AgrarmonitorService(config); +} + +describe('AgrarmonitorService.resetCookies', () => { + beforeEach(() => storeClear.mockClear()); + + it('setzt über die laufende Instanz zurück und verwirft sie', async () => { + const s = service(); + const clearSession = jest.fn(() => Promise.resolve()); + (s as unknown as { client: Partial }).client = + { clearSession }; + + const result = await s.resetCookies(); + + expect(result.success).toBe(true); + expect(clearSession).toHaveBeenCalledTimes(1); + expect(storeClear).not.toHaveBeenCalled(); + expect((s as unknown as { client: unknown }).client).toBeNull(); + }); + + it('löscht ohne Instanz direkt die Cookie-Datei, ohne sich anzumelden', async () => { + const result = await service().resetCookies(); + + expect(result.success).toBe(true); + expect(storeClear).toHaveBeenCalledTimes(1); + }); +}); diff --git a/paperless-backend/src/agrarmonitor/agrarmonitor.service.ts b/paperless-backend/src/agrarmonitor/agrarmonitor.service.ts index 0d9cf76..3bb27f2 100644 --- a/paperless-backend/src/agrarmonitor/agrarmonitor.service.ts +++ b/paperless-backend/src/agrarmonitor/agrarmonitor.service.ts @@ -46,21 +46,7 @@ export class AgrarmonitorService { 'https://api.agrarmonitor.de', ); const apiToken = this.configService.get('AGRARMONITOR_API_TOKEN'); - const cookiePath = this.configService.get( - 'AGRARMONITOR_COOKIE_PATH', - './data/agrarmonitor-cookies.json', - ); - const encryptionKey = this.configService.get( - 'AGRARMONITOR_ENCRYPTION_KEY', - ); - - const encryptor = encryptionKey - ? new AesGcmCookieEncryptor(encryptionKey) - : undefined; - const cookieStore = new FileCookieStore(cookiePath, { - encryptor, - logger: this.logger, - }); + const cookieStore = this.createCookieStore(); this.client = await createAgrarmonitorClient({ baseUrl, @@ -79,10 +65,45 @@ export class AgrarmonitorService { return this.client; } + private createCookieStore(): FileCookieStore { + const cookiePath = this.configService.get( + 'AGRARMONITOR_COOKIE_PATH', + './data/agrarmonitor-cookies.json', + ); + const encryptionKey = this.configService.get( + 'AGRARMONITOR_ENCRYPTION_KEY', + ); + const encryptor = encryptionKey + ? new AesGcmCookieEncryptor(encryptionKey) + : undefined; + return new FileCookieStore(cookiePath, { + encryptor, + logger: this.logger, + }); + } + clearClient(): void { this.client = null; } + /** + * Löscht alle gespeicherten Agrarmonitor-Cookies (Session). Der nächste + * Zugriff meldet sich neu an. + */ + async resetCookies(): Promise { + const client = this.client; + this.client = null; + // Über die laufende Instanz zurücksetzen – sonst schreibt sie ihren alten + // Cookie-Jar beim nächsten Request wieder in die Datei. + if (client) await client.clearSession(); + else await this.createCookieStore().clear(); + this.logger.warn('Agrarmonitor-Cookies wurden zurückgesetzt'); + return { + success: true, + message: 'Cookies gelöscht – beim nächsten Zugriff wird neu angemeldet.', + }; + } + async getStatus(): Promise { try { const client = await this.getClient(); diff --git a/paperless-frontend/src/api/settings.ts b/paperless-frontend/src/api/settings.ts index 420ff16..c8f9744 100644 --- a/paperless-frontend/src/api/settings.ts +++ b/paperless-frontend/src/api/settings.ts @@ -317,6 +317,8 @@ export const agrarmonitorApi = { api.post('/api/agrarmonitor/sync-correspondents').then((r) => r.data), mergeCorrespondents: (keepId: number, deleteId: number) => api.post<{ mergedDocuments: number }>('/api/agrarmonitor/merge-correspondents', { keepId, deleteId }).then((r) => r.data), + resetCookies: () => + api.post<{ success: boolean; message: string }>('/api/agrarmonitor/reset-cookies').then((r) => r.data), getLieferantennummernStatus: () => api.get('/api/agrarmonitor/lieferantennummern/status').then((r) => r.data), lieferantennummernVorschau: () => diff --git a/paperless-frontend/src/pages/SettingsPage.tsx b/paperless-frontend/src/pages/SettingsPage.tsx index 6b07a36..a0a9ad0 100644 --- a/paperless-frontend/src/pages/SettingsPage.tsx +++ b/paperless-frontend/src/pages/SettingsPage.tsx @@ -2529,6 +2529,7 @@ function AgrarmonitorTab() { const [pollingForm] = Form.useForm(); const [loading, setLoading] = useState(false); const [registering, setRegistering] = useState(false); + const [cookiesResetting, setCookiesResetting] = useState(false); const [pollingConfigLoading, setPollingConfigLoading] = useState(false); const [pollingSaving, setPollingSaving] = useState(false); const [pollingRunning, setPollingRunning] = useState(false); @@ -2555,6 +2556,20 @@ function AgrarmonitorTab() { } }; + const handleResetCookies = async () => { + setCookiesResetting(true); + try { + const result = await agrarmonitorApi.resetCookies(); + setStatus(null); + setRegisterResult(null); + message.success(result.message); + } catch (err) { + message.error(fehlermeldung(err, 'Cookies konnten nicht gelöscht werden')); + } finally { + setCookiesResetting(false); + } + }; + const handleRegister = async () => { const values = await form.validateFields(); setRegistering(true); @@ -2646,9 +2661,29 @@ function AgrarmonitorTab() {
- + + + + Die gespeicherte Agrarmonitor-Session wird verworfen. Beim nächsten Zugriff meldet sich + der Manager neu an. Möglicherweise muss das Gerät danach neu registriert bzw. + freigeschaltet werden. +
+ } + okText="Löschen" + okButtonProps={{ danger: true }} + cancelText="Abbrechen" + onConfirm={handleResetCookies} + > + + +
{status && (