From 52dcdce8bb0d5ef36dd3691230d60d5a17b6eca9 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Tue, 17 Feb 2026 13:33:39 -0600 Subject: [PATCH] update --- public/app.js | 20 ++++++++++++++-- public/index.html | 33 +++++++++++++++++++++++++- qbo_helper.js | 42 +++++++++++++++++++++++++++------ server.js | 53 ++++++++++++++++++++++++++++++++++++++++-- set_qbo_token.js | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 set_qbo_token.js diff --git a/public/app.js b/public/app.js index 75eda88..d35af27 100644 --- a/public/app.js +++ b/public/app.js @@ -82,6 +82,18 @@ document.addEventListener('DOMContentLoaded', () => { setDefaultDate(); checkCurrentLogo(); + // *** FIX 3: Gespeicherten Tab wiederherstellen (oder 'quotes' als Default) *** + const savedTab = localStorage.getItem('activeTab') || 'quotes'; + showTab(savedTab); + + // Hash-basierte Tab-Navigation (z.B. nach OAuth Redirect /#settings) + if (window.location.hash) { + const hashTab = window.location.hash.replace('#', ''); + if (['quotes', 'invoices', 'customers', 'settings'].includes(hashTab)) { + showTab(hashTab); + } + } + // Setup form handlers document.getElementById('customer-form').addEventListener('submit', handleCustomerSubmit); document.getElementById('quote-form').addEventListener('submit', handleQuoteSubmit); @@ -108,6 +120,9 @@ function showTab(tabName) { document.getElementById(`${tabName}-tab`).classList.remove('hidden'); document.getElementById(`tab-${tabName}`).classList.add('bg-blue-800'); + // *** FIX 3: Tab-Auswahl persistieren *** + localStorage.setItem('activeTab', tabName); + if (tabName === 'quotes') { loadQuotes(); } else if (tabName === 'invoices') { @@ -1113,6 +1128,7 @@ function viewInvoicePDF(id) { window.open(`/api/invoices/${id}/pdf`, '_blank'); } +// *** FIX 2: Verbesserte Erfolgsmeldung mit QBO DocNumber *** async function exportToQBO(id) { if (!confirm('Rechnung wirklich an QuickBooks Online senden?')) return; @@ -1127,8 +1143,8 @@ async function exportToQBO(id) { const result = await response.json(); if (response.ok) { - alert(`✅ Erfolg! QBO ID: ${result.qbo_id}`); - // Optional: Liste neu laden um Status zu aktualisieren + alert(`✅ Erfolg! QBO ID: ${result.qbo_id}, Rechnungsnr: ${result.qbo_doc_number}`); + // Liste neu laden um aktualisierte invoice_number anzuzeigen loadInvoices(); } else { alert(`❌ Fehler: ${result.error}`); diff --git a/public/index.html b/public/index.html index 092b57c..b3ed647 100644 --- a/public/index.html +++ b/public/index.html @@ -147,7 +147,38 @@
-
+
+ +

QuickBooks Online Authorization

+

+ Wenn der Token abgelaufen ist oder die Verbindung fehlschlägt, + hier neu autorisieren. Du wirst zu Intuit weitergeleitet. +

+ +
+ + 🔑 Authorize QBO + + Checking... +
+ +

QuickBooks Online Connection Test

Test the connection and token refresh logic by fetching a report of overdue invoices (> 30 days) directly from QBO.

diff --git a/qbo_helper.js b/qbo_helper.js index 3e11bf6..72102e7 100644 --- a/qbo_helper.js +++ b/qbo_helper.js @@ -1,4 +1,4 @@ -// qbo_helper.js - FIX FÜR ERROR 3200 +// qbo_helper.js - MIT OAUTH FLOW & VERBESSERTEM TOKEN HANDLING require('dotenv').config(); const OAuthClient = require('intuit-oauth'); const fs = require('fs'); @@ -31,7 +31,7 @@ const getOAuthClient = () => { console.error("❌ Fehler beim Laden des gespeicherten Tokens:", e.message); } - if (savedToken) { + if (savedToken && savedToken.refresh_token) { oauthClient.setToken(savedToken); console.log("✅ Gespeicherter Token aus qbo_token.json geladen."); } else { @@ -40,13 +40,25 @@ const getOAuthClient = () => { refresh_token: process.env.QBO_REFRESH_TOKEN || '', realmId: process.env.QBO_REALM_ID }; - oauthClient.setToken(envToken); - console.log("ℹ️ Token aus .env geladen (Fallback)."); + 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(); @@ -60,6 +72,12 @@ function saveTokens() { 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 { @@ -68,7 +86,15 @@ async function makeQboApiCall(requestOptions) { saveTokens(); return authResponse; } catch (e) { - console.error("❌ Refresh fehlgeschlagen:", e.originalMessage || 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; } }; @@ -82,7 +108,7 @@ async function makeQboApiCall(requestOptions) { if (data.fault && data.fault.error) { const errorCode = data.fault.error[0].code; - // --- FIX: 3200 (Auth Failed) HINZUGEFÜGT --- + // 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(); @@ -114,5 +140,7 @@ async function makeQboApiCall(requestOptions) { module.exports = { getOAuthClient, - makeQboApiCall + makeQboApiCall, + saveTokens, + resetOAuthClient }; \ No newline at end of file diff --git a/server.js b/server.js index 78c10df..9957e10 100644 --- a/server.js +++ b/server.js @@ -4,7 +4,8 @@ const path = require('path'); const puppeteer = require('puppeteer'); const fs = require('fs').promises; const multer = require('multer'); -const { makeQboApiCall, getOAuthClient } = require('./qbo_helper'); +const OAuthClient = require('intuit-oauth'); +const { makeQboApiCall, getOAuthClient, saveTokens, resetOAuthClient } = require('./qbo_helper'); const app = express(); const PORT = process.env.PORT || 3000; @@ -1253,7 +1254,7 @@ app.post('/api/invoices/:id/export', async (req, res) => { // 6. DB Update: Wir speichern AUCH die QBO-Nummer, damit wir wissen, wie sie drüben heißt await client.query( - `UPDATE invoices SET qbo_id = $1, qbo_sync_token = $2, qbo_doc_number = $3 WHERE id = $4`, + `UPDATE invoices SET qbo_id = $1, qbo_sync_token = $2, qbo_doc_number = $3, invoice_number = $3 WHERE id = $4`, [qboInvoice.Id, qboInvoice.SyncToken, qboInvoice.DocNumber, id] ); @@ -1307,6 +1308,54 @@ app.get('/api/qbo/overdue', async (req, res) => { res.status(500).json({ error: error.message }); } }); +// Schritt 1: User klickt "Authorize" → Redirect zu Intuit +app.get('/auth/qbo', (req, res) => { + const client = getOAuthClient(); + const authUri = client.authorizeUri({ + scope: [OAuthClient.scopes.Accounting], + state: 'intuit-qbo-auth' + }); + console.log('🔗 Redirecting to QBO Authorization:', authUri); + res.redirect(authUri); +}); + +// Schritt 2: Intuit redirected zurück mit Code → Token holen +app.get('/auth/qbo/callback', async (req, res) => { + const client = getOAuthClient(); + try { + const authResponse = await client.createToken(req.url); + console.log('✅ QBO Authorization erfolgreich!'); + saveTokens(); + + // Redirect zurück zur App (Settings Tab) + res.redirect('/#settings'); + } catch (e) { + console.error('❌ QBO Authorization fehlgeschlagen:', e); + res.status(500).send(` +

QBO Authorization Failed

+

${e.message || e}

+ Zurück zur App + `); + } +}); + +// Status-Check Endpoint (für die UI) +app.get('/api/qbo/status', (req, res) => { + try { + const client = getOAuthClient(); + const token = client.getToken(); + const hasToken = !!(token && token.refresh_token); + res.json({ + connected: hasToken, + realmId: token?.realmId || null + }); + } catch (e) { + res.json({ connected: false }); + } +}); + + + // Start server and browser async function startServer() { await initBrowser(); diff --git a/set_qbo_token.js b/set_qbo_token.js new file mode 100644 index 0000000..9931fd6 --- /dev/null +++ b/set_qbo_token.js @@ -0,0 +1,59 @@ +#!/usr/bin/env node +// ===================================================== +// set_qbo_token.js +// +// Einmalig ausführen um qbo_token.json korrekt zu setzen. +// Die intuit-oauth Library braucht ein vollständiges Token-Objekt, +// nicht nur access_token + refresh_token. +// +// Verwendung: +// node set_qbo_token.js +// +// Beispiel: +// node set_qbo_token.js "eyJlbmMi..." "AB11..." "9341..." +// ===================================================== + +const fs = require('fs'); +const path = require('path'); + +const accessToken = process.argv[2]; +const refreshToken = process.argv[3]; +const realmId = process.argv[4]; + +if (!accessToken || !refreshToken || !realmId) { + console.log(''); + console.log('Verwendung:'); + console.log(' node set_qbo_token.js '); + console.log(''); + console.log('Die Werte bekommst du aus dem Intuit OAuth Playground:'); + console.log(' https://developer.intuit.com/app/developer/playground'); + console.log(''); + process.exit(1); +} + +// Das ist das Format, das die intuit-oauth Library erwartet +const tokenObject = { + token_type: "bearer", + access_token: accessToken, + refresh_token: refreshToken, + expires_in: 3600, + x_refresh_token_expires_in: 8726400, + realmId: realmId, + // createdAt wird von der Library geprüft um zu sehen ob der Token abgelaufen ist + createdAt: new Date().toISOString() +}; + +const tokenFile = path.join(__dirname, 'qbo_token.json'); +fs.writeFileSync(tokenFile, JSON.stringify(tokenObject, null, 2)); + +console.log(''); +console.log('✅ qbo_token.json erfolgreich erstellt!'); +console.log(` 📁 ${tokenFile}`); +console.log(` 🔑 Access Token: ${accessToken.substring(0, 20)}...`); +console.log(` 🔄 Refresh Token: ${refreshToken.substring(0, 15)}...`); +console.log(` 🏢 Realm ID: ${realmId}`); +console.log(''); +console.log('Nächste Schritte:'); +console.log(' 1. Docker Container neu starten: docker compose restart quote_app'); +console.log(' 2. In Settings → "Test Connection" klicken'); +console.log(''); \ No newline at end of file