Amazon Business API integration replacing browser automation
- Add amazon_api.py with Reconciliation + Document API client - OAuth flow with manual code exchange for local installations - Dual mode: API (recommended) or Browser automation (fallback) - New settings: amazon_app_id, amazon_client_id, amazon_client_secret, amazon_refresh_token - Platform UI with mode switcher, API credential fields, OAuth button - Scheduler supports both API and browser modes - README with full Amazon API setup guide - httpx added for async HTTP requests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+203
-36
@@ -17,6 +17,13 @@
|
||||
<option value="true" {% if settings.get('amazon_enabled') == 'true' %}selected{% endif %}>Aktiviert</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="amazon_mode">Abruf-Modus</label>
|
||||
<select id="amazon_mode" name="amazon_mode" onchange="toggleAmazonMode()">
|
||||
<option value="api" {% if settings.get('amazon_mode') == 'api' %}selected{% endif %}>API (empfohlen)</option>
|
||||
<option value="browser" {% if settings.get('amazon_mode', 'browser') == 'browser' %}selected{% endif %}>Browser-Automation</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="amazon_domain">Amazon-Domain</label>
|
||||
<select id="amazon_domain" name="amazon_domain">
|
||||
@@ -25,29 +32,71 @@
|
||||
<option value="amazon.com" {% if settings.get('amazon_domain') == 'amazon.com' %}selected{% endif %}>amazon.com</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="amazon_email">Amazon E-Mail</label>
|
||||
<input type="email" id="amazon_email" name="amazon_email"
|
||||
value="{{ settings.get('amazon_email', '') }}"
|
||||
placeholder="email@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="amazon_password">Amazon Passwort</label>
|
||||
<input type="password" id="amazon_password" name="amazon_password"
|
||||
placeholder="{% if settings.get('amazon_password') %}(gespeichert){% else %}Passwort eingeben{% endif %}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="amazon_since_date">Rechnungen ab Datum</label>
|
||||
<input type="date" id="amazon_since_date" name="amazon_since_date"
|
||||
value="{{ settings.get('amazon_since_date', '') }}">
|
||||
<small class="text-muted">Leer = letzte 30 Tage</small>
|
||||
</div>
|
||||
<div class="form-group" style="align-self:end;">
|
||||
{% if settings.get('amazon_last_sync') %}
|
||||
<small class="text-muted">Letzter Abruf: {{ settings.get('amazon_last_sync') }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- API Mode Fields -->
|
||||
<div id="apiFields" style="{% if settings.get('amazon_mode', 'browser') != 'api' %}display:none;{% endif %}margin-top:1rem;">
|
||||
<h3 style="font-size:1rem;margin-bottom:0.75rem;">API-Zugangsdaten (Amazon Business API)</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="amazon_app_id">App-ID</label>
|
||||
<input type="text" id="amazon_app_id" name="amazon_app_id"
|
||||
value="{{ settings.get('amazon_app_id', '') }}"
|
||||
placeholder="amzn1.sp.solution.xxxxx">
|
||||
<small class="text-muted">Aus dem Solution Provider Portal</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="amazon_client_id">Client-ID (LWA)</label>
|
||||
<input type="text" id="amazon_client_id" name="amazon_client_id"
|
||||
value="{{ settings.get('amazon_client_id', '') }}"
|
||||
placeholder="amzn1.application-oa2-client.xxxxx">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="amazon_client_secret">Client-Sicherheitsschluessel (LWA)</label>
|
||||
<input type="password" id="amazon_client_secret" name="amazon_client_secret"
|
||||
placeholder="{% if settings.get('amazon_client_secret') %}(gespeichert){% else %}Client Secret eingeben{% endif %}">
|
||||
</div>
|
||||
<div class="form-group" style="align-self:end;">
|
||||
<small class="text-muted">
|
||||
{% if settings.get('amazon_refresh_token') %}
|
||||
Refresh-Token: gespeichert
|
||||
{% else %}
|
||||
Refresh-Token: fehlt - bitte autorisieren
|
||||
{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Browser Mode Fields -->
|
||||
<div id="browserFields" style="{% if settings.get('amazon_mode', 'browser') != 'browser' %}display:none;{% endif %}margin-top:1rem;">
|
||||
<h3 style="font-size:1rem;margin-bottom:0.75rem;">Browser-Zugangsdaten</h3>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="amazon_email">Amazon E-Mail</label>
|
||||
<input type="email" id="amazon_email" name="amazon_email"
|
||||
value="{{ settings.get('amazon_email', '') }}"
|
||||
placeholder="email@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="amazon_password">Amazon Passwort</label>
|
||||
<input type="password" id="amazon_password" name="amazon_password"
|
||||
placeholder="{% if settings.get('amazon_password') %}(gespeichert){% else %}Passwort eingeben{% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:1rem;">
|
||||
{% if settings.get('amazon_last_sync') %}
|
||||
<small class="text-muted">Letzter Abruf: {{ settings.get('amazon_last_sync') }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-actions" style="margin-top:1rem;">
|
||||
<button type="button" class="btn btn-primary" onclick="saveAmazonSettings()">Einstellungen speichern</button>
|
||||
</div>
|
||||
@@ -57,16 +106,41 @@
|
||||
<div class="card">
|
||||
<h2>Anmeldung & Abruf</h2>
|
||||
<div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:1rem;">
|
||||
<span>Session:</span>
|
||||
<span>Status:</span>
|
||||
<span id="sessionBadge" class="badge badge-inactive">Wird geprüft...</span>
|
||||
</div>
|
||||
|
||||
<div style="display:flex;gap:0.75rem;flex-wrap:wrap;">
|
||||
<button type="button" id="btnLogin" class="btn btn-primary" onclick="doLogin()">Bei Amazon anmelden</button>
|
||||
<button type="button" id="btnLogout" class="btn btn-secondary" onclick="doLogout()" style="display:none;">Session löschen</button>
|
||||
<button type="button" id="btnProcess" class="btn btn-success" onclick="doProcess()" style="display:none;">Jetzt Rechnungen abrufen</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="doReset()">Importierte zurücksetzen</button>
|
||||
<!-- API Mode Buttons -->
|
||||
<div id="apiButtons" style="display:none;">
|
||||
<div style="display:flex;gap:0.75rem;flex-wrap:wrap;align-items:center;">
|
||||
<button type="button" id="btnOAuth" class="btn btn-primary" onclick="doOAuth()">Bei Amazon autorisieren</button>
|
||||
<button type="button" id="btnProcessApi" class="btn btn-success" onclick="doProcess()" style="display:none;">Jetzt Rechnungen abrufen</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="doReset()">Importierte zurücksetzen</button>
|
||||
</div>
|
||||
<!-- Manual code exchange for local setups -->
|
||||
<div id="oauthCodeBox" style="display:none;margin-top:1rem;">
|
||||
<p class="text-muted" style="font-size:0.85rem;">
|
||||
Nach der Autorisierung bei Amazon werden Sie zu einer Seite weitergeleitet die nicht laden wird.
|
||||
Kopieren Sie den Wert von <code>spapi_oauth_code</code> aus der URL-Leiste und tragen ihn hier ein:
|
||||
</p>
|
||||
<div style="display:flex;gap:0.5rem;align-items:center;">
|
||||
<input type="text" id="oauthCode" placeholder="spapi_oauth_code hier einfuegen..."
|
||||
style="flex:1;padding:0.5rem;border:1px solid var(--border-color);border-radius:4px;background:var(--input-bg);color:var(--text-color);">
|
||||
<button type="button" class="btn btn-primary" onclick="exchangeCode()">Token tauschen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Browser Mode Buttons -->
|
||||
<div id="browserButtons">
|
||||
<div style="display:flex;gap:0.75rem;flex-wrap:wrap;">
|
||||
<button type="button" id="btnLogin" class="btn btn-primary" onclick="doLogin()">Bei Amazon anmelden</button>
|
||||
<button type="button" id="btnLogout" class="btn btn-secondary" onclick="doLogout()" style="display:none;">Session löschen</button>
|
||||
<button type="button" id="btnProcess" class="btn btn-success" onclick="doProcess()" style="display:none;">Jetzt Rechnungen abrufen</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="doReset()">Importierte zurücksetzen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="processMsg" style="margin-top:0.75rem;"></div>
|
||||
</div>
|
||||
|
||||
@@ -97,6 +171,16 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- Mode Toggle ---
|
||||
function toggleAmazonMode() {
|
||||
const mode = document.getElementById('amazon_mode').value;
|
||||
document.getElementById('apiFields').style.display = mode === 'api' ? '' : 'none';
|
||||
document.getElementById('browserFields').style.display = mode === 'browser' ? '' : 'none';
|
||||
document.getElementById('apiButtons').style.display = mode === 'api' ? '' : 'none';
|
||||
document.getElementById('browserButtons').style.display = mode === 'browser' ? '' : 'none';
|
||||
checkSession();
|
||||
}
|
||||
|
||||
// --- Settings ---
|
||||
async function saveAmazonSettings() {
|
||||
const btn = event.target;
|
||||
@@ -106,10 +190,14 @@ async function saveAmazonSettings() {
|
||||
|
||||
const data = {
|
||||
amazon_enabled: document.getElementById('amazon_enabled').value,
|
||||
amazon_mode: document.getElementById('amazon_mode').value,
|
||||
amazon_domain: document.getElementById('amazon_domain').value,
|
||||
amazon_email: document.getElementById('amazon_email').value,
|
||||
amazon_password: document.getElementById('amazon_password').value,
|
||||
amazon_since_date: document.getElementById('amazon_since_date').value,
|
||||
amazon_app_id: document.getElementById('amazon_app_id').value,
|
||||
amazon_client_id: document.getElementById('amazon_client_id').value,
|
||||
amazon_client_secret: document.getElementById('amazon_client_secret').value,
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -142,21 +230,43 @@ async function checkSession() {
|
||||
try {
|
||||
const resp = await fetch('/api/amazon-status');
|
||||
const data = await resp.json();
|
||||
if (data.login_active) {
|
||||
badge.className = 'badge badge-warning';
|
||||
badge.textContent = 'Login läuft...';
|
||||
document.getElementById('btnLogout').style.display = 'none';
|
||||
document.getElementById('btnProcess').style.display = 'none';
|
||||
} else if (data.session_valid) {
|
||||
badge.className = 'badge badge-success';
|
||||
badge.textContent = 'Angemeldet';
|
||||
document.getElementById('btnLogout').style.display = '';
|
||||
document.getElementById('btnProcess').style.display = '';
|
||||
|
||||
if (data.mode === 'api') {
|
||||
// API mode
|
||||
if (data.api_authorized) {
|
||||
badge.className = 'badge badge-success';
|
||||
badge.textContent = 'API autorisiert';
|
||||
document.getElementById('btnOAuth').style.display = 'none';
|
||||
document.getElementById('btnProcessApi').style.display = '';
|
||||
} else if (data.api_configured) {
|
||||
badge.className = 'badge badge-warning';
|
||||
badge.textContent = 'Nicht autorisiert';
|
||||
document.getElementById('btnOAuth').style.display = '';
|
||||
document.getElementById('btnProcessApi').style.display = 'none';
|
||||
} else {
|
||||
badge.className = 'badge badge-inactive';
|
||||
badge.textContent = 'API nicht konfiguriert';
|
||||
document.getElementById('btnOAuth').style.display = 'none';
|
||||
document.getElementById('btnProcessApi').style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
badge.className = 'badge badge-inactive';
|
||||
badge.textContent = 'Nicht angemeldet';
|
||||
document.getElementById('btnLogout').style.display = 'none';
|
||||
document.getElementById('btnProcess').style.display = 'none';
|
||||
// Browser mode
|
||||
if (data.login_active) {
|
||||
badge.className = 'badge badge-warning';
|
||||
badge.textContent = 'Login läuft...';
|
||||
document.getElementById('btnLogout').style.display = 'none';
|
||||
document.getElementById('btnProcess').style.display = 'none';
|
||||
} else if (data.session_valid) {
|
||||
badge.className = 'badge badge-success';
|
||||
badge.textContent = 'Angemeldet';
|
||||
document.getElementById('btnLogout').style.display = '';
|
||||
document.getElementById('btnProcess').style.display = '';
|
||||
} else {
|
||||
badge.className = 'badge badge-inactive';
|
||||
badge.textContent = 'Nicht angemeldet';
|
||||
document.getElementById('btnLogout').style.display = 'none';
|
||||
document.getElementById('btnProcess').style.display = 'none';
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
badge.className = 'badge badge-inactive';
|
||||
@@ -164,6 +274,62 @@ async function checkSession() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- OAuth Authorization (API mode) ---
|
||||
async function doOAuth() {
|
||||
const msgEl = document.getElementById('processMsg');
|
||||
try {
|
||||
const resp = await fetch('/api/amazon-oauth-url');
|
||||
const data = await resp.json();
|
||||
if (data.url) {
|
||||
window.open(data.url, '_blank');
|
||||
// Show code input box for manual exchange
|
||||
document.getElementById('oauthCodeBox').style.display = '';
|
||||
msgEl.innerHTML = '<div class="alert alert-info">Amazon-Autorisierungsseite wurde geöffnet. Nach der Autorisierung den Code aus der URL kopieren und unten eintragen.</div>';
|
||||
} else {
|
||||
msgEl.innerHTML = '<div class="alert alert-error">' + escapeHtml(data.error || 'Fehler') + '</div>';
|
||||
}
|
||||
} catch (e) {
|
||||
msgEl.innerHTML = '<div class="alert alert-error">Verbindungsfehler</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function exchangeCode() {
|
||||
const codeInput = document.getElementById('oauthCode');
|
||||
let code = codeInput.value.trim();
|
||||
const msgEl = document.getElementById('processMsg');
|
||||
|
||||
if (!code) {
|
||||
msgEl.innerHTML = '<div class="alert alert-error">Bitte den Code eintragen</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// If user pasted the full URL, extract the code
|
||||
if (code.includes('spapi_oauth_code=')) {
|
||||
const url = new URL(code.startsWith('http') ? code : 'https://x.com?' + code);
|
||||
code = url.searchParams.get('spapi_oauth_code') || code;
|
||||
}
|
||||
|
||||
msgEl.innerHTML = '<div class="alert alert-info">Token wird getauscht...</div>';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/amazon-oauth-exchange', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({code}),
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
msgEl.innerHTML = '<div class="alert alert-success">Erfolgreich autorisiert! Refresh-Token gespeichert.</div>';
|
||||
document.getElementById('oauthCodeBox').style.display = 'none';
|
||||
checkSession();
|
||||
} else {
|
||||
msgEl.innerHTML = '<div class="alert alert-error">' + escapeHtml(data.error || 'Token-Exchange fehlgeschlagen') + '</div>';
|
||||
}
|
||||
} catch (e) {
|
||||
msgEl.innerHTML = '<div class="alert alert-error">Verbindungsfehler</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Interactive Browser Login ---
|
||||
let screenshotInterval = null;
|
||||
let loginPollInterval = null;
|
||||
@@ -357,7 +523,8 @@ function escapeHtml(str) {
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// Initial check
|
||||
// Initial setup
|
||||
toggleAmazonMode();
|
||||
checkSession();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user