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:
+120
-4
@@ -31,6 +31,12 @@ from app.amazon_processor import (
|
||||
close_interactive_login as amazon_close_interactive,
|
||||
is_interactive_login_active as amazon_login_active,
|
||||
)
|
||||
from app.amazon_api import (
|
||||
get_oauth_authorize_url,
|
||||
exchange_auth_code,
|
||||
check_api_configured,
|
||||
process_amazon_api,
|
||||
)
|
||||
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, os.environ.get("LOG_LEVEL", "INFO").upper(), logging.INFO),
|
||||
@@ -388,6 +394,10 @@ async def api_amazon_settings(request: Request):
|
||||
"amazon_email": body.get("amazon_email", ""),
|
||||
"amazon_password": body.get("amazon_password") or current.get("amazon_password", ""),
|
||||
"amazon_since_date": body.get("amazon_since_date", ""),
|
||||
"amazon_mode": body.get("amazon_mode", "browser"),
|
||||
"amazon_app_id": body.get("amazon_app_id", ""),
|
||||
"amazon_client_id": body.get("amazon_client_id", ""),
|
||||
"amazon_client_secret": body.get("amazon_client_secret") or current.get("amazon_client_secret", ""),
|
||||
}
|
||||
await save_settings(data)
|
||||
return JSONResponse({"success": True})
|
||||
@@ -395,9 +405,110 @@ async def api_amazon_settings(request: Request):
|
||||
|
||||
@app.get("/api/amazon-status")
|
||||
async def api_amazon_status():
|
||||
valid = await amazon_check_session()
|
||||
login_active = amazon_login_active()
|
||||
return JSONResponse({"session_valid": valid, "login_active": login_active})
|
||||
settings = await get_settings()
|
||||
mode = settings.get("amazon_mode", "browser")
|
||||
|
||||
if mode == "api":
|
||||
api_status = await check_api_configured()
|
||||
return JSONResponse({
|
||||
"mode": "api",
|
||||
"session_valid": api_status.get("authorized", False),
|
||||
"login_active": False,
|
||||
"api_configured": api_status.get("configured", False),
|
||||
"api_authorized": api_status.get("authorized", False),
|
||||
})
|
||||
else:
|
||||
valid = await amazon_check_session()
|
||||
login_active = amazon_login_active()
|
||||
return JSONResponse({
|
||||
"mode": "browser",
|
||||
"session_valid": valid,
|
||||
"login_active": login_active,
|
||||
})
|
||||
|
||||
|
||||
def _get_oauth_redirect_uri(request: Request) -> str:
|
||||
"""Get OAuth redirect URI from env var or request."""
|
||||
base = os.environ.get("OAUTH_REDIRECT_BASE", "").rstrip("/")
|
||||
if not base:
|
||||
base = str(request.base_url).rstrip("/")
|
||||
return f"{base}/api/amazon-oauth-callback"
|
||||
|
||||
|
||||
@app.get("/api/amazon-oauth-url")
|
||||
async def api_amazon_oauth_url(request: Request):
|
||||
"""Generate OAuth authorization URL for Amazon Business API."""
|
||||
settings = await get_settings()
|
||||
app_id = settings.get("amazon_app_id", "")
|
||||
if not app_id:
|
||||
return JSONResponse({"error": "App-ID nicht konfiguriert"}, status_code=400)
|
||||
|
||||
redirect_uri = _get_oauth_redirect_uri(request)
|
||||
domain = settings.get("amazon_domain", "amazon.de")
|
||||
state = str(uuid.uuid4())
|
||||
|
||||
url = get_oauth_authorize_url(app_id, redirect_uri, domain, state)
|
||||
return JSONResponse({"url": url, "state": state})
|
||||
|
||||
|
||||
@app.get("/api/amazon-oauth-callback")
|
||||
async def api_amazon_oauth_callback(request: Request):
|
||||
"""Handle OAuth callback from Amazon."""
|
||||
code = request.query_params.get("spapi_oauth_code") or request.query_params.get("code", "")
|
||||
error = request.query_params.get("error", "")
|
||||
|
||||
if error:
|
||||
return HTMLResponse(f"<h2>Autorisierung fehlgeschlagen</h2><p>{error}</p><p>Fenster kann geschlossen werden.</p>")
|
||||
|
||||
if not code:
|
||||
return HTMLResponse("<h2>Fehler: Kein Autorisierungscode erhalten</h2><p>Fenster kann geschlossen werden.</p>")
|
||||
|
||||
settings = await get_settings()
|
||||
client_id = settings.get("amazon_client_id", "")
|
||||
client_secret = settings.get("amazon_client_secret", "")
|
||||
redirect_uri = _get_oauth_redirect_uri(request)
|
||||
|
||||
result = await exchange_auth_code(code, client_id, client_secret, redirect_uri)
|
||||
|
||||
if "error" in result:
|
||||
return HTMLResponse(f"<h2>Token-Exchange fehlgeschlagen</h2><p>{result['error']}</p>")
|
||||
|
||||
refresh_token = result.get("refresh_token", "")
|
||||
if refresh_token:
|
||||
await save_settings({"amazon_refresh_token": refresh_token})
|
||||
return HTMLResponse(
|
||||
"<h2>Autorisierung erfolgreich!</h2>"
|
||||
"<p>Refresh-Token wurde gespeichert. Dieses Fenster kann geschlossen werden.</p>"
|
||||
"<script>window.close();</script>"
|
||||
)
|
||||
|
||||
return HTMLResponse("<h2>Fehler: Kein Refresh-Token erhalten</h2>")
|
||||
|
||||
|
||||
@app.post("/api/amazon-oauth-exchange")
|
||||
async def api_amazon_oauth_exchange(request: Request):
|
||||
"""Manual OAuth code exchange - user pastes the code from the redirect URL."""
|
||||
body = await request.json()
|
||||
code = body.get("code", "").strip()
|
||||
if not code:
|
||||
return JSONResponse({"error": "Kein Code angegeben"}, status_code=400)
|
||||
|
||||
settings = await get_settings()
|
||||
client_id = settings.get("amazon_client_id", "")
|
||||
client_secret = settings.get("amazon_client_secret", "")
|
||||
redirect_uri = _get_oauth_redirect_uri(request)
|
||||
|
||||
result = await exchange_auth_code(code, client_id, client_secret, redirect_uri)
|
||||
|
||||
if "error" in result:
|
||||
return JSONResponse({"error": result["error"]}, status_code=400)
|
||||
|
||||
refresh_token = result.get("refresh_token", "")
|
||||
if refresh_token:
|
||||
await save_settings({"amazon_refresh_token": refresh_token})
|
||||
return JSONResponse({"success": True})
|
||||
|
||||
return JSONResponse({"error": "Kein Refresh-Token erhalten"}, status_code=400)
|
||||
|
||||
|
||||
@app.post("/api/amazon-login")
|
||||
@@ -462,7 +573,12 @@ async def api_amazon_logout():
|
||||
|
||||
@app.post("/api/amazon-process")
|
||||
async def api_amazon_process():
|
||||
result = await process_amazon()
|
||||
settings = await get_settings()
|
||||
mode = settings.get("amazon_mode", "browser")
|
||||
if mode == "api":
|
||||
result = await process_amazon_api()
|
||||
else:
|
||||
result = await process_amazon()
|
||||
return JSONResponse(result)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user