The login flow was near-silent: three info lines with no way to see why a session was considered expired or what happened to the cookies. Debugging a failing login meant guessing. Every login step now logs on debug level with a `phase` field: the login page fetch, the extracted nonce, the POST to /login/api/login.php, the session check, and any request retried after a re-login. Each of those requests is bracketed by a cookie snapshot taken before it is sent and one taken after, plus a delta of added/changed/removed cookies. The "before" snapshot is logged prior to sending, so it survives a hanging request. isLoginRequiredResponse() is split so the criterion that matched (status-401, login-markup, ...) can be reported instead of just a boolean. Secrets stay out of the log: the password is replaced by ***, and cookie values - including those in the raw Set-Cookie header - are truncated to their first four characters plus length. That still shows whether a session id changed without putting the session itself in the log. Detail logging is strictly debug-level via logLoginDetail(); the existing logDebug() falls back to info, which would flood a plain info logger. When no debug logger is attached, no snapshot is taken and nothing is serialized. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
106 lines
5.6 KiB
Markdown
106 lines
5.6 KiB
Markdown
# Agrarmonitor Connector
|
|
|
|
TypeScript MVP connector for Agrarmonitor with shared cookie persistence, optional AES-GCM cookie encryption, automatic login, retry after expired sessions, device registration checks, and customer detail extraction.
|
|
|
|
Login uses Agrarmonitor's redirect flow only: the connector loads `/`, extracts the `nonce`, and posts credentials to `/redirect.php?id=benutzerverwaltung&action=login`.
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
npm install
|
|
```
|
|
|
|
## Build
|
|
|
|
```bash
|
|
npm run build
|
|
```
|
|
|
|
## Example
|
|
|
|
```ts
|
|
import {
|
|
AesGcmCookieEncryptor,
|
|
FileCookieStore,
|
|
createAgrarmonitorClient,
|
|
} from 'agrarmonitor-connector';
|
|
|
|
const agrarmonitor = await createAgrarmonitorClient({
|
|
baseUrl: 'https://admin7.agrarmonitor.de',
|
|
apiToken: process.env.AGRARMONITOR_API_TOKEN,
|
|
username: process.env.AGRARMONITOR_USERNAME ?? '',
|
|
password: process.env.AGRARMONITOR_PASSWORD ?? '',
|
|
cookieStore: new FileCookieStore(
|
|
process.env.AGRARMONITOR_COOKIE_PATH ?? './data/agrarmonitor-cookies.json',
|
|
{
|
|
encryptor: process.env.AGRARMONITOR_ENCRYPTION_KEY
|
|
? new AesGcmCookieEncryptor(process.env.AGRARMONITOR_ENCRYPTION_KEY)
|
|
: undefined,
|
|
logger: console,
|
|
}
|
|
),
|
|
logger: console,
|
|
});
|
|
|
|
const freischaltung = await agrarmonitor.checkFreigeschaltet();
|
|
console.log(freischaltung.freigeschaltet);
|
|
|
|
const registrierung = await agrarmonitor.checkRegistriert();
|
|
console.log(registrierung.registriert);
|
|
|
|
const response = await agrarmonitor.http.get('/kunden/detail/123');
|
|
console.log(response.status);
|
|
```
|
|
|
|
## Login-Logging
|
|
|
|
Der Login-Flow protokolliert jeden Schritt auf `debug`-Level, sofern der uebergebene Logger eine `debug`-Methode hat. Ohne `debug`-Methode bleiben diese Details still - ein reiner `info`-Logger sieht davon nichts.
|
|
|
|
Protokolliert werden der Aufruf der Loginseite, die gelesene Nonce, der `POST /login/api/login.php`, die Session-Pruefung und jeder Request, der wegen abgelaufener Session wiederholt wird. Zu jedem dieser Requests steht ein Cookie-Snapshot **vor** dem Absenden und einer **danach** im Log, dazu ein Delta mit neuen, geaenderten und entfernten Cookies:
|
|
|
|
```
|
|
[debug] Login-POST: POST /login/api/login.php
|
|
{"phase":"request","method":"POST","url":"/login/api/login.php","cookies":[],
|
|
"body":{"username":"demo","passwort":"***","nonce":"9f3a1c7b2e...","ssoAction":"","ssoReturn":""}}
|
|
[debug] Login-POST: POST /login/api/login.php -> 200
|
|
{"phase":"response","status":200,"istLoginSeite":false,
|
|
"setCookie":["PHPSESSID=f81d…(len=32); Path=/; HttpOnly"],
|
|
"cookies":[{"name":"PHPSESSID","value":"f81d…(len=32)","domain":"127.0.0.1","path":"/",
|
|
"expires":"Session","httpOnly":true,"secure":false}],
|
|
"cookieChanges":{"added":["PHPSESSID"],"changed":[],"removed":[]}}
|
|
```
|
|
|
|
Passwort und Cookie-Werte stehen nie im Klartext im Log: das Passwort wird durch `***` ersetzt, Cookie-Werte und der rohe `Set-Cookie`-Header werden auf die ersten vier Zeichen plus Laenge gekuerzt. Damit bleibt erkennbar, ob sich eine Session-ID geaendert hat, ohne dass das Log selbst die Session preisgibt.
|
|
|
|
Das `phase`-Feld ordnet jeden Eintrag zu: `login-start`, `request`, `response`, `nonce`, `session-check`, `session-expired`, `retry`, `error`, `login-done`.
|
|
|
|
## Cookie Persistence
|
|
|
|
`FileCookieStore` keeps one shared `CookieJar` per file path inside the Node process. Multiple connector instances that use the same cookie file therefore reuse the same session and every successful request saves the latest cookies back to disk.
|
|
|
|
The store can read both the connector format and the older Telefonbuch cookie-array format.
|
|
|
|
## Useful Methods
|
|
|
|
- `checkFreigeschaltet()` checks whether Agrarmonitor redirects to `/freischaltung/`.
|
|
- `checkRegistriert()` checks whether the page still contains `Neues Gerät registrieren`.
|
|
- `registerDevice({ agrarmonitorId, pcName })` loads `/freischaltung/`, extracts the nonce, and posts the registration request.
|
|
- `fetchCustomers()` loads customers from `https://api.agrarmonitor.de/v1/kunden`.
|
|
- `getKunden2()` loads all customer pages and returns normalized customer/supplier fields.
|
|
- `getCustomerById(id)` loads one customer from the token-based Agrarmonitor API.
|
|
- `eingangsrechnungenLivesearch(suchstring)` searches invoice files and enriches matching Eingangsrechnungen with edit/detail metadata.
|
|
- `eingangsrechnungVorhanden(suchstring)` checks whether invoice file search has rows.
|
|
- `eingangsrechnungImDateieingangVorhanden(suchstring)` checks whether file inbox search has rows.
|
|
- `getRechnungsdaten(rechnungId)` reads editable invoice fields.
|
|
- `setRechnungsdaten(rechnungId, daten)` updates editable invoice fields.
|
|
- `setLieferscheinNummer(rechnungId, nummer)` updates only the delivery note number.
|
|
- `setEingangsdatum(rechnungId, datum)` updates the received date.
|
|
- `getMaschinenKategorien()` reads `/maschinen/kategorien` and returns ID, name, tractor flag, and mobile visibility.
|
|
- `getMaschinen(gruppenId, suchstring?)` reads `/module/maschinen/livesearch.php` and returns ID, number, name, license plate, and active flag.
|
|
- `getFirmen()` reads `/einstellungen` and returns company ID and description.
|
|
- `getArtikelEinheiten()` reads `/artikel/einheiten` and returns unit ID, description, and short label.
|
|
- `getArtikelKategorien()` reads `/artikel/kategorien` and returns product group ID and name.
|
|
- `getArtikel(artikelGruppe, suchstring?)` reads `/module/artikel/livesearch.php` and returns article ID, number, and description.
|
|
- `getMitarbeiter(suchstring?, mitarbeitergruppe?)` reads `/module/mitarbeiter/livesearch.php` and returns employee ID, number, last name, first name, and active flag.
|
|
- `saveSession()` explicitly persists the current cookie jar.
|