Skip to content

Commit e449b49

Browse files
committed
fix: ebay oauth
1 parent 85c9bee commit e449b49

5 files changed

Lines changed: 36 additions & 65 deletions

File tree

api/EBAY.md

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,6 @@ In the developer portal, configure OAuth:
2020

2121
**Important:** no mismatch in trailing slash, `http` vs `https`, or host (`localhost` vs `127.0.0.1`) between the eBay portal, `EBAY_REDIRECT_URI`, and the URL actually opened in the browser.
2222

23-
### Local dev / desktop (Tauri) — “j’ai l’impression de passer sur une vieille version”
24-
25-
After you approve eBay, **the browser always lands on `EBAY_REDIRECT_URI`**, not on whatever tab you started from.
26-
27-
If `EBAY_REDIRECT_URI` points to **production** (e.g. `https://goupixdex.example/settings/marketplaces`) but you started OAuth from **`http://localhost:3000`** or a **Tauri dev** window, you will be redirected to **that production URL**. You then load the **deployed** front-end: no Nuxt devtools, no floating “Redémarrer workers / Sync DB” (those only exist when `import.meta.dev` is true), and the UI may lag behind your local branch.
28-
29-
**Fix:** for local work, register and set `EBAY_REDIRECT_URI` to the **same origin** as the app you are running (e.g. `http://localhost:3000/settings/marketplaces` or your Tauri dev URL if eBay allows it). Use a separate eBay keyset/sandbox RuName if needed.
30-
3123
## 3. Scopes used by GoupixDex
3224

3325
The API requests these scopes (already set in the backend):
@@ -73,7 +65,6 @@ Fulfillment shipping options (multiple domestic rates, international, handling t
7365

7466
## 8. Quick troubleshooting
7567

76-
- **Après « Se connecter à eBay », l’app ressemble à une vieille version / plus d’outils dev** : eBay vous renvoie toujours vers `EBAY_REDIRECT_URI`. Si cette URL est la **production** alors que vous étiez en **localhost** ou Tauri dev, vous chargez le site déployé (build sans mode dev). Alignez `EBAY_REDIRECT_URI` + RuName eBay sur l’origine où vous travaillez — voir §2 ci-dessus.
7768
- **Token exchange error**: `redirect_uri` does not match what is registered at eBay, or `code` already used / expired (codes are single-use and short-lived).
7869
- **“User is not eligible for Business Policy”** (logs: error 20403 on `fulfillment_policy`): the account must be enrolled in **business policies**. The API calls [`optInToProgram`](https://developer.ebay.com/api-docs/sell/account/resources/program/methods/optInToProgram) with `SELLING_POLICY_MANAGEMENT` before loading policies; eBay can take **up to ~24 h** to activate — retry later or check with `getOptedInPrograms`.
7970
- **Publishing error**: wrong category, policies incompatible with the marketplace, or **condition descriptors** required for some card categories — see API logs (`ebay_body`).

api/routes/ebay_route.py

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

55
import datetime as dt
6+
import logging
67
from typing import Annotated, Any
78
from urllib.parse import unquote
89

10+
logger = logging.getLogger(__name__)
11+
912
from fastapi import APIRouter, Depends, HTTPException, Query, status
1013
from pydantic import BaseModel, Field
1114
from sqlalchemy.orm import Session
@@ -83,18 +86,16 @@ def ebay_authorize_url(
8386
force_login: Annotated[bool, Query()] = False,
8487
) -> dict[str, str]:
8588
"""Build the browser URL for eBay consent (User must open it)."""
86-
app = get_settings()
8789
try:
88-
url = build_authorization_url(state=state, force_login=force_login, app=app)
90+
url = build_authorization_url(state=state, force_login=force_login)
8991
except RuntimeError as exc:
9092
if str(exc) == "ebay_oauth_not_configured":
9193
raise HTTPException(
9294
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
9395
detail="eBay OAuth is not configured on the server (EBAY_CLIENT_ID, EBAY_CLIENT_SECRET, EBAY_REDIRECT_URI).",
9496
) from exc
9597
raise HTTPException(status_code=500, detail=str(exc)) from exc
96-
redirect = (app.ebay_redirect_uri or "").strip()
97-
return {"authorization_url": url, "state": state, "redirect_uri": redirect}
98+
return {"authorization_url": url, "state": state}
9899

99100

100101
@router.post("/oauth/exchange", status_code=status.HTTP_200_OK)
@@ -135,12 +136,36 @@ async def ebay_oauth_exchange(
135136
db.add(user)
136137
db.commit()
137138
db.refresh(user)
138-
scope = token_scope_string(data)
139+
140+
# Auth-code exchange often omits ``scope``; refresh with all consented scopes so the access
141+
# token includes sell.fulfillment (required for getOrders / shipping labels).
142+
scope_data = data
143+
rt_plain = decrypt_ebay_token(user.ebay_refresh_token)
144+
if rt_plain:
145+
try:
146+
scope_data = await refresh_user_access_token(rt_plain, request_all_scopes=True)
147+
access = scope_data.get("access_token")
148+
if access and isinstance(access, str):
149+
user.ebay_access_token = store_ebay_token(access)
150+
expires_in = int(scope_data.get("expires_in", 7200))
151+
user.ebay_access_expires_at = now + dt.timedelta(seconds=expires_in)
152+
if scope_data.get("refresh_token"):
153+
user.ebay_refresh_token = store_ebay_token(str(scope_data["refresh_token"]))
154+
db.add(user)
155+
db.commit()
156+
db.refresh(user)
157+
except Exception as exc:
158+
logger.warning("eBay post-exchange refresh failed (scopes may be incomplete): %s", exc)
159+
160+
scope = token_scope_string(scope_data)
161+
has_fulfillment = token_has_fulfillment_scope(scope_data) or (
162+
not scope and bool(decrypt_ebay_token(user.ebay_refresh_token))
163+
)
139164
return {
140165
"ok": True,
141166
"ebay_connected": bool(decrypt_ebay_token(user.ebay_refresh_token)),
142167
"oauth_scope": scope,
143-
"has_fulfillment_scope": token_has_fulfillment_scope(data),
168+
"has_fulfillment_scope": has_fulfillment,
144169
}
145170

146171

api/services/ebay_publish_service.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
_api_base_url,
2424
refresh_user_access_token,
2525
token_has_fulfillment_scope,
26+
token_scope_string,
2627
)
2728
from services.ebay_trading_card_grading import (
2829
EBAY_CONDITION_GRADED,
@@ -262,7 +263,8 @@ async def ensure_ebay_access_token(
262263
"on your production app keyset (developer.ebay.com)."
263264
) from exc
264265
raise
265-
if require_fulfillment_scope and not token_has_fulfillment_scope(data):
266+
scope_str = token_scope_string(data)
267+
if require_fulfillment_scope and scope_str and not token_has_fulfillment_scope(data):
266268
raise RuntimeError(
267269
"ebay_fulfillment_scope_missing: le token OAuth ne contient pas sell.fulfillment. "
268270
"Activez ce scope sur developer.ebay.com (clés production), révoquez l'app sur eBay.fr, "

web/app/layouts/default.vue

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,7 @@ const toast = useToast()
7575
const workersRestarting: Ref<boolean> = ref(false)
7676
const dbSyncLoading: Ref<boolean> = ref(false)
7777
78-
const showDevWorkerControls = computed(
79-
() =>
80-
// Production / site déployé : import.meta.dev est false → pas de panneau (ex. retour OAuth eBay vers EBAY_REDIRECT_URI prod).
81-
import.meta.dev && Boolean(isDesktopApp.value),
82-
)
78+
const showDevWorkerControls = computed(() => import.meta.dev && Boolean(isDesktopApp.value))
8379
8480
async function onRestartWorkers(): Promise<void> {
8581
workersRestarting.value = true

web/app/pages/settings/marketplaces.vue

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -242,60 +242,17 @@ async function startEbayOAuth(): Promise<void> {
242242
sessionStorage.setItem('ebay_oauth_state', state)
243243
}
244244
try {
245-
const { data } = await $api.get<{
246-
authorization_url: string
247-
redirect_uri?: string
248-
}>('/ebay/oauth/authorize-url', {
245+
const { data } = await $api.get<{ authorization_url: string }>('/ebay/oauth/authorize-url', {
249246
params: { state, force_login: true },
250247
})
251248
if (import.meta.client) {
252-
const expected = normalizeOAuthCallbackUrl(`${window.location.origin}/settings/marketplaces`)
253-
const configured = normalizeOAuthCallbackUrl(data.redirect_uri || '')
254-
if (configured && configured !== expected) {
255-
const ok = window.confirm(
256-
[
257-
"L'URL de retour eBay configurée sur le serveur ne correspond pas à cette fenêtre.",
258-
'',
259-
`Serveur (EBAY_REDIRECT_URI) :\n${data.redirect_uri}`,
260-
'',
261-
`Cette app :\n${window.location.origin}/settings/marketplaces`,
262-
'',
263-
'Après validation chez eBay, vous serez renvoyé vers la première URL — souvent la production : autre interface, pas d’outils de développement (workers / sync DB).',
264-
'',
265-
'Pour le dev local : alignez EBAY_REDIRECT_URI et l’« URL acceptée » eBay sur la même origine que cette page (voir api/EBAY.md).',
266-
'',
267-
'Continuer quand même ?',
268-
].join('\n'),
269-
)
270-
if (!ok) {
271-
return
272-
}
273-
}
274249
window.location.href = data.authorization_url
275250
}
276251
} catch (e) {
277252
toast.add({ title: 'OAuth indisponible', description: apiErrorMessage(e), color: 'error' })
278253
}
279254
}
280255
281-
/** Same origin + path for comparing server redirect_uri with this app. */
282-
function normalizeOAuthCallbackUrl(raw: string): string {
283-
const t = raw.trim()
284-
if (!t) {
285-
return ''
286-
}
287-
try {
288-
const u = new URL(t)
289-
let path = u.pathname
290-
while (path.length > 1 && path.endsWith('/')) {
291-
path = path.slice(0, -1)
292-
}
293-
return `${u.origin}${path === '' ? '/' : path}`.toLowerCase()
294-
} catch {
295-
return t.replace(/\/+$/, '').toLowerCase()
296-
}
297-
}
298-
299256
async function submitOnboarding(): Promise<void> {
300257
if (!phone.value.trim() || !addressLine1.value.trim() || !city.value.trim() || !postalCode.value.trim()) {
301258
toast.add({

0 commit comments

Comments
 (0)