neuer Ansatz ...

This commit is contained in:
Andreas Knuth 2026-02-17 15:05:26 -06:00
parent 03e0516c08
commit 31f03b0d7c
1 changed files with 51 additions and 24 deletions

View File

@ -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(); require('dotenv').config();
const OAuthClient = require('intuit-oauth'); const OAuthClient = require('intuit-oauth');
const fs = require('fs'); const fs = require('fs');
@ -35,10 +44,6 @@ const getOAuthClient = () => {
oauthClient.setToken(savedToken); oauthClient.setToken(savedToken);
console.log("✅ Gespeicherter Token aus qbo_token.json geladen."); console.log("✅ Gespeicherter Token aus qbo_token.json geladen.");
} else { } 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 = { const envToken = {
token_type: 'bearer', token_type: 'bearer',
access_token: process.env.QBO_ACCESS_TOKEN || '', access_token: process.env.QBO_ACCESS_TOKEN || '',
@ -52,25 +57,39 @@ const getOAuthClient = () => {
oauthClient.setToken(envToken); oauthClient.setToken(envToken);
console.log(" Token aus .env geladen (Fallback)."); console.log(" Token aus .env geladen (Fallback).");
} else { } else {
console.warn("⚠️ Kein gültiger Token vorhanden. Bitte unter Settings → QBO autorisieren."); console.warn("⚠️ Kein gültiger Token vorhanden.");
} }
} }
} }
return oauthClient; return oauthClient;
}; };
/**
* Setzt den oauthClient zurück, damit beim nächsten getOAuthClient()
* der Token frisch aus der Datei geladen wird.
*/
function resetOAuthClient() { function resetOAuthClient() {
oauthClient = null; oauthClient = null;
} }
function saveTokens() { function saveTokens() {
try { try {
const token = getOAuthClient().getToken(); const client = getOAuthClient();
fs.writeFileSync(tokenFile, JSON.stringify(token, null, 2)); 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."); console.log("💾 Tokens erfolgreich in qbo_token.json gespeichert.");
} catch (e) { } catch (e) {
console.error("❌ Fehler beim Speichern der Tokens:", e.message); console.error("❌ Fehler beim Speichern der Tokens:", e.message);
@ -80,27 +99,39 @@ function saveTokens() {
async function makeQboApiCall(requestOptions) { async function makeQboApiCall(requestOptions) {
const client = getOAuthClient(); const client = getOAuthClient();
// Prüfen ob überhaupt ein Refresh Token vorhanden ist
const currentToken = client.getToken(); const currentToken = client.getToken();
if (!currentToken || !currentToken.refresh_token) { 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 () => { const doRefresh = async () => {
console.log("🔄 QBO Token Refresh wird ausgeführt..."); 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 { try {
const authResponse = await client.refresh(); // KRITISCHER FIX: refreshUsingToken() statt refresh() verwenden!
console.log("✅ Token erfolgreich erneuert."); //
// 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(); saveTokens();
return authResponse; return authResponse;
} catch (e) { } catch (e) {
const errMsg = e.originalMessage || e.message || String(e); const errMsg = e.originalMessage || e.message || String(e);
console.error("❌ Refresh fehlgeschlagen:", errMsg); 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_grant')) {
if (errMsg.includes('invalid') || errMsg.includes('Authorize again')) {
throw new Error( 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; throw e;
@ -110,17 +141,14 @@ async function makeQboApiCall(requestOptions) {
try { try {
const response = await client.makeApiCall(requestOptions); 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; const data = response.getJson ? response.getJson() : response.json;
if (data.fault && data.fault.error) { if (data.fault && data.fault.error) {
const errorCode = data.fault.error[0].code; const errorCode = data.fault.error[0].code;
// 3200 (Auth Failed), 3202, 3100 → Refresh versuchen
if (errorCode === '3200' || errorCode === '3202' || errorCode === '3100') { if (errorCode === '3200' || errorCode === '3202' || errorCode === '3100') {
console.log(`⚠️ QBO meldet Token-Fehler (${errorCode}). Versuche Refresh und Retry...`); console.log(`⚠️ QBO meldet Token-Fehler (${errorCode}). Versuche Refresh und Retry...`);
await doRefresh(); await doRefresh();
// Retry mit neuem Token
return await client.makeApiCall(requestOptions); return await client.makeApiCall(requestOptions);
} }
throw new Error(`QBO API Error ${errorCode}: ${data.fault.error[0].message}`); throw new Error(`QBO API Error ${errorCode}: ${data.fault.error[0].message}`);
@ -130,7 +158,6 @@ async function makeQboApiCall(requestOptions) {
return response; return response;
} catch (e) { } catch (e) {
// HTTP 401 Unauthorized fangen (falls die Lib wirft, statt data.fault zurückzugeben)
const isAuthError = const isAuthError =
e.response?.status === 401 || e.response?.status === 401 ||
(e.authResponse && e.authResponse.response && e.authResponse.response.status === 401) || (e.authResponse && e.authResponse.response && e.authResponse.response.status === 401) ||