// 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'); const path = require('path'); let oauthClient = null; const tokenFile = path.join(__dirname, 'qbo_token.json'); const getOAuthClient = () => { if (!oauthClient) { oauthClient = new OAuthClient({ clientId: process.env.QBO_CLIENT_ID, clientSecret: process.env.QBO_CLIENT_SECRET, environment: process.env.QBO_ENVIRONMENT || 'sandbox', redirectUri: process.env.QBO_REDIRECT_URI }); let savedToken = null; try { if (fs.existsSync(tokenFile)) { const stat = fs.statSync(tokenFile); if (stat.isFile()) { const content = fs.readFileSync(tokenFile, 'utf8'); if (content.trim() !== "{}") { savedToken = JSON.parse(content); } } } } catch (e) { console.error("❌ Fehler beim Laden des gespeicherten Tokens:", e.message); } if (savedToken && savedToken.refresh_token) { oauthClient.setToken(savedToken); console.log("✅ Gespeicherter Token aus qbo_token.json geladen."); } else { const envToken = { token_type: 'bearer', access_token: process.env.QBO_ACCESS_TOKEN || '', refresh_token: process.env.QBO_REFRESH_TOKEN || '', expires_in: 3600, x_refresh_token_expires_in: 8726400, realmId: process.env.QBO_REALM_ID, createdAt: new Date().toISOString() }; if (envToken.refresh_token) { oauthClient.setToken(envToken); console.log("ℹ️ Token aus .env geladen (Fallback)."); } else { console.warn("⚠️ Kein gültiger Token vorhanden."); } } } return oauthClient; }; function resetOAuthClient() { oauthClient = null; } function saveTokens() { try { 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); } } async function makeQboApiCall(requestOptions) { const client = getOAuthClient(); const currentToken = client.getToken(); if (!currentToken || !currentToken.refresh_token) { 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 { // 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); if (errMsg.includes('invalid_grant')) { throw new Error( "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; } }; try { const response = await client.makeApiCall(requestOptions); const data = response.getJson ? response.getJson() : response.json; if (data.fault && data.fault.error) { const errorCode = data.fault.error[0].code; if (errorCode === '3200' || errorCode === '3202' || errorCode === '3100') { console.log(`⚠️ QBO meldet Token-Fehler (${errorCode}). Versuche Refresh und Retry...`); await doRefresh(); return await client.makeApiCall(requestOptions); } throw new Error(`QBO API Error ${errorCode}: ${data.fault.error[0].message}`); } saveTokens(); return response; } catch (e) { const isAuthError = e.response?.status === 401 || (e.authResponse && e.authResponse.response && e.authResponse.response.status === 401) || e.message?.includes('AuthenticationFailed'); if (isAuthError) { console.log("⚠️ 401 Unauthorized / AuthFailed erhalten. Versuche Refresh und Retry..."); await doRefresh(); return await client.makeApiCall(requestOptions); } throw e; } } module.exports = { getOAuthClient, makeQboApiCall, saveTokens, resetOAuthClient };