Skip to content

Commit 646963e

Browse files
committed
fix: ebay token conflicts
1 parent c599972 commit 646963e

3 files changed

Lines changed: 88 additions & 6 deletions

File tree

api/services/ebay_oauth_service.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import base64
6+
import json
67
import logging
78
from typing import Any
89
from urllib.parse import urlencode
@@ -20,6 +21,57 @@
2021
]
2122

2223

24+
class EbayOAuthError(RuntimeError):
25+
"""Erreur OAuth eBay exploitable côté UI (code + message lisibles)."""
26+
27+
def __init__(self, code: str, message: str, *, status: int | None = None) -> None:
28+
super().__init__(code)
29+
self.code = code
30+
self.message = message
31+
self.status = status
32+
33+
def __str__(self) -> str:
34+
return self.message or self.code
35+
36+
37+
def _describe_ebay_oauth_error(status: int, body: str) -> EbayOAuthError:
38+
"""Transforme un body 4xx eBay (`{"error": "...", "error_description": "..."}`)
39+
en exception `RuntimeError` lisible, avec un code applicatif stable pour l'UI."""
40+
payload: dict[str, Any] = {}
41+
try:
42+
parsed = json.loads(body) if body else {}
43+
if isinstance(parsed, dict):
44+
payload = parsed
45+
except ValueError:
46+
payload = {}
47+
err = str(payload.get("error") or "").strip()
48+
desc = str(payload.get("error_description") or "").strip()
49+
50+
if err == "invalid_scope":
51+
return EbayOAuthError(
52+
"ebay_scope_mismatch",
53+
"Votre consentement eBay n'inclut pas tous les scopes demandés. "
54+
"Reconnectez votre compte eBay dans les Paramètres pour accorder les nouveaux accès.",
55+
status=status,
56+
)
57+
if err in {"invalid_grant", "invalid_token"}:
58+
return EbayOAuthError(
59+
"ebay_refresh_token_invalid",
60+
"Le refresh token eBay a expiré ou a été révoqué. Reconnectez votre compte eBay.",
61+
status=status,
62+
)
63+
if err == "invalid_client":
64+
return EbayOAuthError(
65+
"ebay_client_invalid",
66+
"Identifiants application eBay invalides (client_id / client_secret). "
67+
"Vérifiez la configuration serveur.",
68+
status=status,
69+
)
70+
71+
message = desc or err or f"Erreur eBay ({status})."
72+
return EbayOAuthError(err or "ebay_token_http_error", message, status=status)
73+
74+
2375
def _auth_base_url(app: AppSettings) -> str:
2476
return "https://auth.sandbox.ebay.com" if app.ebay_use_sandbox else "https://auth.ebay.com"
2577

@@ -78,19 +130,28 @@ async def exchange_authorization_code(code: str, *, app: AppSettings | None = No
78130
resp = await client.post(url, data=data, headers=headers)
79131
if resp.status_code >= 400:
80132
logger.warning("eBay token exchange failed: %s %s", resp.status_code, resp.text[:500])
81-
resp.raise_for_status()
133+
raise _describe_ebay_oauth_error(resp.status_code, resp.text)
82134
return resp.json()
83135

84136

85137
async def refresh_user_access_token(refresh_token: str, *, app: AppSettings | None = None) -> dict[str, Any]:
138+
"""Rafraîchit l'access token utilisateur.
139+
140+
On ne force **volontairement pas** le paramètre ``scope`` : eBay renvoie alors
141+
un token portant les scopes tels qu'ils ont été consentis au moment de la
142+
connexion. C'est indispensable quand ``EBAY_SCOPES`` évolue (ajout de
143+
``sell.fulfillment`` par exemple) : les utilisateurs déjà connectés avant
144+
l'ajout continuent de publier (inventory + account) sans reconsentement ;
145+
les fonctionnalités nécessitant le nouveau scope détecteront son absence et
146+
proposeront la reconnexion.
147+
"""
86148
s = app or get_settings()
87149
if not ebay_oauth_configured(s):
88150
raise RuntimeError("ebay_oauth_not_configured")
89151
url = f"{_api_base_url(s)}/identity/v1/oauth2/token"
90152
data = {
91153
"grant_type": "refresh_token",
92154
"refresh_token": refresh_token.strip(),
93-
"scope": " ".join(EBAY_SCOPES),
94155
}
95156
headers = {
96157
"Content-Type": "application/x-www-form-urlencoded",
@@ -100,5 +161,5 @@ async def refresh_user_access_token(refresh_token: str, *, app: AppSettings | No
100161
resp = await client.post(url, data=data, headers=headers)
101162
if resp.status_code >= 400:
102163
logger.warning("eBay refresh failed: %s %s", resp.status_code, resp.text[:500])
103-
resp.raise_for_status()
164+
raise _describe_ebay_oauth_error(resp.status_code, resp.text)
104165
return resp.json()

api/services/ebay_publish_service.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,9 +285,15 @@ async def publish_article_to_ebay(
285285
try:
286286
await _emit_ebay(progress, "auth", "Connecting to the eBay API…")
287287
token = await ensure_ebay_access_token(db, user, app=s)
288-
except RuntimeError as exc:
289-
await _emit_ebay(progress, "error", "Could not obtain an eBay token.", detail=str(exc))
290-
return {"ok": False, "detail": str(exc)}
288+
except Exception as exc:
289+
# Inclut RuntimeError (ebay_not_connected, ebay_oauth_not_configured, …)
290+
# et EbayOAuthError (invalid_scope, invalid_grant, …). On ne laisse plus
291+
# fuir httpx.HTTPStatusError : le log « auth → error » doit toujours être
292+
# émis pour que le Journal des publications affiche la vraie raison.
293+
logger.warning("eBay token acquisition failed for user=%s: %s", user.id, exc)
294+
detail = str(exc) or exc.__class__.__name__
295+
await _emit_ebay(progress, "error", "Could not obtain an eBay token.", detail=detail)
296+
return {"ok": False, "detail": detail}
291297

292298
await _emit_ebay(progress, "auth", "eBay token obtained.")
293299
root = _api_base_url(s)

web/app/composables/useVintedPublishStream.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,21 @@ export function useVintedPublishStream() {
112112
close()
113113
const v = data.vinted
114114
const eb = data.ebay
115+
if (context === 'logs') {
116+
// Journal des publications : on matérialise les échecs dans la
117+
// liste de logs (sinon l'utilisateur ne voit qu'un `done` muet).
118+
if (v && v.published === false) {
119+
const detail
120+
= v.detail === 'missing_vinted_credentials'
121+
? "Identifiants Vinted manquants (profil ou variables d'environnement)."
122+
: String(v.detail ?? 'Publication non confirmée.')
123+
logEntries.value.push({ text: `[Vinted · Erreur] ${detail}` })
124+
}
125+
if (eb && eb.published === false) {
126+
const detail = String(eb.detail ?? 'Échec ou annulation.')
127+
logEntries.value.push({ text: `[eBay · Erreur] ${detail}` })
128+
}
129+
}
115130
if (context !== 'logs') {
116131
if (v) {
117132
if (v.published) {

0 commit comments

Comments
 (0)