From 31f03b0d7cd8a97041d453ef51e43735fb661e50 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Tue, 17 Feb 2026 15:05:26 -0600 Subject: [PATCH] neuer Ansatz ... --- qbo_helper.js | 75 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/qbo_helper.js b/qbo_helper.js index bfb7df6..a9dd7f8 100644 --- a/qbo_helper.js +++ b/qbo_helper.js @@ -1,4 +1,13 @@ -// qbo_helper.js - MIT OAUTH FLOW & VERBESSERTEM TOKEN HANDLING +// qbo_helper.js - DEFINITIVER FIX +// +// Kernproblem: client.refresh() ruft intern validateToken() auf, +// das das Token-Objekt prüft und "invalid" wirft wenn das Format +// nicht stimmt. Das passiert LOKAL, nicht bei Intuit. +// +// Lösung: refreshUsingToken(refreshTokenString) verwenden. +// Diese Methode akzeptiert den RT direkt als String und umgeht +// die validateToken()-Prüfung komplett. + require('dotenv').config(); const OAuthClient = require('intuit-oauth'); const fs = require('fs'); @@ -35,10 +44,6 @@ const getOAuthClient = () => { oauthClient.setToken(savedToken); console.log("✅ Gespeicherter Token aus qbo_token.json geladen."); } else { - // WICHTIG: intuit-oauth braucht ein VOLLSTÄNDIGES Token-Objekt! - // Nur access_token + refresh_token reicht NICHT — die Library - // prüft intern auf token_type, expires_in, createdAt etc. - // und wirft "The Refresh token is invalid" wenn die fehlen. const envToken = { token_type: 'bearer', access_token: process.env.QBO_ACCESS_TOKEN || '', @@ -52,25 +57,39 @@ const getOAuthClient = () => { oauthClient.setToken(envToken); console.log("ℹ️ Token aus .env geladen (Fallback)."); } else { - console.warn("⚠️ Kein gültiger Token vorhanden. Bitte unter Settings → QBO autorisieren."); + console.warn("⚠️ Kein gültiger Token vorhanden."); } } } return oauthClient; }; -/** - * Setzt den oauthClient zurück, damit beim nächsten getOAuthClient() - * der Token frisch aus der Datei geladen wird. - */ function resetOAuthClient() { oauthClient = null; } function saveTokens() { try { - const token = getOAuthClient().getToken(); - fs.writeFileSync(tokenFile, JSON.stringify(token, null, 2)); + const client = getOAuthClient(); + const token = client.getToken(); + + // Debug: Was genau bekommen wir vom Client? + console.log("💾 Speichere Token... refresh_token vorhanden:", !!token.refresh_token, + "| access_token Länge:", (token.access_token || '').length, + "| realmId:", token.realmId || 'FEHLT'); + + // Sicherstellen dass alle Pflichtfelder vorhanden sind + const tokenToSave = { + token_type: token.token_type || 'bearer', + access_token: token.access_token, + refresh_token: token.refresh_token, + expires_in: token.expires_in || 3600, + x_refresh_token_expires_in: token.x_refresh_token_expires_in || 8726400, + realmId: token.realmId || process.env.QBO_REALM_ID, + createdAt: token.createdAt || new Date().toISOString() + }; + + fs.writeFileSync(tokenFile, JSON.stringify(tokenToSave, null, 2)); console.log("💾 Tokens erfolgreich in qbo_token.json gespeichert."); } catch (e) { console.error("❌ Fehler beim Speichern der Tokens:", e.message); @@ -80,27 +99,39 @@ function saveTokens() { async function makeQboApiCall(requestOptions) { const client = getOAuthClient(); - // Prüfen ob überhaupt ein Refresh Token vorhanden ist const currentToken = client.getToken(); if (!currentToken || !currentToken.refresh_token) { - throw new Error("Kein gültiger QBO Token vorhanden. Bitte unter Settings → 'Authorize QBO' klicken."); + throw new Error("Kein gültiger QBO Token vorhanden. Bitte Token erneuern."); } const doRefresh = async () => { console.log("🔄 QBO Token Refresh wird ausgeführt..."); + + // Den Refresh Token als String extrahieren + const refreshTokenStr = currentToken.refresh_token; + console.log("🔑 Refresh Token (erste 15 Zeichen):", refreshTokenStr.substring(0, 15) + "..."); + try { - const authResponse = await client.refresh(); - console.log("✅ Token erfolgreich erneuert."); - saveTokens(); + // KRITISCHER FIX: refreshUsingToken() statt refresh() verwenden! + // + // refresh() ruft intern validateToken() auf, das bei unvollständigem + // Token-Objekt "The Refresh token is invalid" wirft — OHNE jemals + // Intuit zu kontaktieren. + // + // refreshUsingToken() akzeptiert den RT als String und umgeht das. + const authResponse = await client.refreshUsingToken(refreshTokenStr); + console.log("✅ Token erfolgreich erneuert via refreshUsingToken()."); + saveTokens(); return authResponse; } catch (e) { const errMsg = e.originalMessage || e.message || String(e); console.error("❌ Refresh fehlgeschlagen:", errMsg); + if (e.intuit_tid) console.error(" intuit_tid:", e.intuit_tid); - // Wenn der Refresh Token komplett ungültig ist → klare Meldung - if (errMsg.includes('invalid') || errMsg.includes('Authorize again')) { + if (errMsg.includes('invalid_grant')) { throw new Error( - "Der Refresh Token ist abgelaufen. Bitte unter Settings → 'Authorize QBO' neu autorisieren." + "Der Refresh Token ist bei Intuit ungültig (invalid_grant). " + + "Bitte im Playground einen neuen Token holen und set_qbo_token.js ausführen." ); } throw e; @@ -110,17 +141,14 @@ async function makeQboApiCall(requestOptions) { try { const response = await client.makeApiCall(requestOptions); - // Prüfen, ob die Antwort JSON ist (manche Auth-Fehler sind HTML/Text) const data = response.getJson ? response.getJson() : response.json; if (data.fault && data.fault.error) { const errorCode = data.fault.error[0].code; - // 3200 (Auth Failed), 3202, 3100 → Refresh versuchen if (errorCode === '3200' || errorCode === '3202' || errorCode === '3100') { console.log(`⚠️ QBO meldet Token-Fehler (${errorCode}). Versuche Refresh und Retry...`); await doRefresh(); - // Retry mit neuem Token return await client.makeApiCall(requestOptions); } throw new Error(`QBO API Error ${errorCode}: ${data.fault.error[0].message}`); @@ -130,7 +158,6 @@ async function makeQboApiCall(requestOptions) { return response; } catch (e) { - // HTTP 401 Unauthorized fangen (falls die Lib wirft, statt data.fault zurückzugeben) const isAuthError = e.response?.status === 401 || (e.authResponse && e.authResponse.response && e.authResponse.response.status === 401) ||