invoice-system/qbo_helper.js

154 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
};