181 lines
6.7 KiB
JavaScript
181 lines
6.7 KiB
JavaScript
// 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
|
||
}; |