feat(agrarmonitor): Cookies in den Einstellungen zurücksetzen
Build and Push Multi-Platform Images / build-and-push (push) Successful in 42s

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 <noreply@anthropic.com>
This commit is contained in:
bjoernpoettkerandClaude Opus 5.5 committed 2026-09-28 09:53:47 +02:00
1 parent 549948913a
commit 0ff78707d1
5 files changed
+130 -15

No files matched your search

@@ -1,6 +1,7 @@
import { import {
BadRequestException, BadRequestException,
Body, Body,
ConflictException,
Controller, Controller,
Get, Get,
HttpCode, HttpCode,
@@ -36,6 +37,18 @@ export class AgrarmonitorController {
return this.service.registerDevice(body.pcName, body.agrarmonitorId); 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') @Get('polling-config')
@RequirePermissions(Permission.MANAGE_SETTINGS) @RequirePermissions(Permission.MANAGE_SETTINGS)
async getPollingConfig() { async getPollingConfig() {
@@ -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<AgrarmonitorConnectorResult> }).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);
});
});
@@ -46,21 +46,7 @@ export class AgrarmonitorService {
'https://api.agrarmonitor.de', 'https://api.agrarmonitor.de',
); );
const apiToken = this.configService.get<string>('AGRARMONITOR_API_TOKEN'); const apiToken = this.configService.get<string>('AGRARMONITOR_API_TOKEN');
const cookiePath = this.configService.get<string>( const cookieStore = this.createCookieStore();
'AGRARMONITOR_COOKIE_PATH',
'./data/agrarmonitor-cookies.json',
);
const encryptionKey = this.configService.get<string>(
'AGRARMONITOR_ENCRYPTION_KEY',
);
const encryptor = encryptionKey
? new AesGcmCookieEncryptor(encryptionKey)
: undefined;
const cookieStore = new FileCookieStore(cookiePath, {
encryptor,
logger: this.logger,
});
this.client = await createAgrarmonitorClient({ this.client = await createAgrarmonitorClient({
baseUrl, baseUrl,
@@ -79,10 +65,45 @@ export class AgrarmonitorService {
return this.client; return this.client;
} }
private createCookieStore(): FileCookieStore {
const cookiePath = this.configService.get<string>(
'AGRARMONITOR_COOKIE_PATH',
'./data/agrarmonitor-cookies.json',
);
const encryptionKey = this.configService.get<string>(
'AGRARMONITOR_ENCRYPTION_KEY',
);
const encryptor = encryptionKey
? new AesGcmCookieEncryptor(encryptionKey)
: undefined;
return new FileCookieStore(cookiePath, {
encryptor,
logger: this.logger,
});
}
clearClient(): void { clearClient(): void {
this.client = null; this.client = null;
} }
/**
* Löscht alle gespeicherten Agrarmonitor-Cookies (Session). Der nächste
* Zugriff meldet sich neu an.
*/
async resetCookies(): Promise<AgrarmonitorRegisterResultDto> {
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<AgrarmonitorStatusDto> { async getStatus(): Promise<AgrarmonitorStatusDto> {
try { try {
const client = await this.getClient(); const client = await this.getClient();
+2
View File
@@ -317,6 +317,8 @@ export const agrarmonitorApi = {
api.post<SyncCorrespondentsResult>('/api/agrarmonitor/sync-correspondents').then((r) => r.data), api.post<SyncCorrespondentsResult>('/api/agrarmonitor/sync-correspondents').then((r) => r.data),
mergeCorrespondents: (keepId: number, deleteId: number) => mergeCorrespondents: (keepId: number, deleteId: number) =>
api.post<{ mergedDocuments: number }>('/api/agrarmonitor/merge-correspondents', { keepId, deleteId }).then((r) => r.data), 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: () => getLieferantennummernStatus: () =>
api.get<LieferantennummerStatus>('/api/agrarmonitor/lieferantennummern/status').then((r) => r.data), api.get<LieferantennummerStatus>('/api/agrarmonitor/lieferantennummern/status').then((r) => r.data),
lieferantennummernVorschau: () => lieferantennummernVorschau: () =>
@@ -2529,6 +2529,7 @@ function AgrarmonitorTab() {
const [pollingForm] = Form.useForm(); const [pollingForm] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [registering, setRegistering] = useState(false); const [registering, setRegistering] = useState(false);
const [cookiesResetting, setCookiesResetting] = useState(false);
const [pollingConfigLoading, setPollingConfigLoading] = useState(false); const [pollingConfigLoading, setPollingConfigLoading] = useState(false);
const [pollingSaving, setPollingSaving] = useState(false); const [pollingSaving, setPollingSaving] = useState(false);
const [pollingRunning, setPollingRunning] = 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 handleRegister = async () => {
const values = await form.validateFields(); const values = await form.validateFields();
setRegistering(true); setRegistering(true);
@@ -2646,9 +2661,29 @@ function AgrarmonitorTab() {
<Space direction="vertical" style={{ width: '100%' }}> <Space direction="vertical" style={{ width: '100%' }}>
<div> <div>
<Space>
<Button loading={loading} onClick={handleLoadStatus}> <Button loading={loading} onClick={handleLoadStatus}>
Status abrufen Status abrufen
</Button> </Button>
<Popconfirm
title="Agrarmonitor-Cookies löschen?"
description={
<div style={{ maxWidth: 320 }}>
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.
</div>
}
okText="Löschen"
okButtonProps={{ danger: true }}
cancelText="Abbrechen"
onConfirm={handleResetCookies}
>
<Button danger loading={cookiesResetting}>
Cookies zurücksetzen
</Button>
</Popconfirm>
</Space>
</div> </div>
{status && ( {status && (