// qbo_helper.js - MIT OAUTH FLOW & VERBESSERTEM TOKEN HANDLING 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 { // 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 || '', 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. Bitte unter Settings → QBO autorisieren."); } } } 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)); 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(); // 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."); } const doRefresh = async () => { console.log("🔄 QBO Token Refresh wird ausgeführt..."); try { const authResponse = await client.refresh(); console.log("✅ Token erfolgreich erneuert."); saveTokens(); return authResponse; } catch (e) { const errMsg = e.originalMessage || e.message || String(e); console.error("❌ Refresh fehlgeschlagen:", errMsg); // Wenn der Refresh Token komplett ungültig ist → klare Meldung if (errMsg.includes('invalid') || errMsg.includes('Authorize again')) { throw new Error( "Der Refresh Token ist abgelaufen. Bitte unter Settings → 'Authorize QBO' neu autorisieren." ); } throw e; } }; 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}`); } saveTokens(); 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) || 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 };