Surface Etsy OAuth errors and use form-encoded token requests
- Callback failures now redirect with the underlying error message, shown in the Settings toast, instead of a generic failure - Token endpoint requests use application/x-www-form-urlencoded per RFC 6749 instead of JSON Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
64aae6adf7
commit
08160775e7
3 changed files with 19 additions and 8 deletions
|
|
@ -51,7 +51,11 @@ const Settings = () => {
|
||||||
toast.success('Etsy shop connected!');
|
toast.success('Etsy shop connected!');
|
||||||
window.history.replaceState({}, '', window.location.pathname);
|
window.history.replaceState({}, '', window.location.pathname);
|
||||||
} else if (etsyResult === 'error') {
|
} else if (etsyResult === 'error') {
|
||||||
toast.error('Etsy connection failed. Please try again.');
|
const reason = params.get('reason');
|
||||||
|
toast.error(
|
||||||
|
reason ? `Etsy connection failed: ${reason}` : 'Etsy connection failed. Please try again.',
|
||||||
|
{ duration: 12000 }
|
||||||
|
);
|
||||||
window.history.replaceState({}, '', window.location.pathname);
|
window.history.replaceState({}, '', window.location.pathname);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
|
|
@ -44,14 +44,15 @@ const loadConfig = async (userId: string): Promise<EtsyConfig | null> => {
|
||||||
|
|
||||||
// Where to send the browser after the OAuth callback: same origin as the
|
// Where to send the browser after the OAuth callback: same origin as the
|
||||||
// redirect URI (the app and API share one domain)
|
// redirect URI (the app and API share one domain)
|
||||||
const settingsUrl = (redirectUri: string | undefined, suffix: string): string => {
|
const settingsUrl = (redirectUri: string | undefined, suffix: string, reason?: string): string => {
|
||||||
let origin = process.env.CLIENT_URL || 'http://localhost:3000';
|
let origin = process.env.CLIENT_URL || 'http://localhost:3000';
|
||||||
try {
|
try {
|
||||||
if (redirectUri) origin = new URL(redirectUri).origin;
|
if (redirectUri) origin = new URL(redirectUri).origin;
|
||||||
} catch {
|
} catch {
|
||||||
// keep fallback
|
// keep fallback
|
||||||
}
|
}
|
||||||
return `${origin}/settings?etsy=${suffix}`;
|
const reasonParam = reason ? `&reason=${encodeURIComponent(reason.slice(0, 300))}` : '';
|
||||||
|
return `${origin}/settings?etsy=${suffix}${reasonParam}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const maskKey = (key: string): string =>
|
const maskKey = (key: string): string =>
|
||||||
|
|
@ -137,7 +138,12 @@ router.get('/callback', async (req: Request, res: Response) => {
|
||||||
const pending = typeof state === 'string' ? pendingAuth.get(state) : undefined;
|
const pending = typeof state === 'string' ? pendingAuth.get(state) : undefined;
|
||||||
|
|
||||||
if (error || typeof code !== 'string' || !pending) {
|
if (error || typeof code !== 'string' || !pending) {
|
||||||
return res.redirect(settingsUrl(pending?.redirectUri, 'error'));
|
const reason = error
|
||||||
|
? `Etsy returned: ${error}${req.query.error_description ? ` — ${req.query.error_description}` : ''}`
|
||||||
|
: !pending
|
||||||
|
? 'Login attempt expired or unknown (possibly a server restart) — try connecting again'
|
||||||
|
: 'No authorization code returned';
|
||||||
|
return res.redirect(settingsUrl(pending?.redirectUri, 'error', reason));
|
||||||
}
|
}
|
||||||
pendingAuth.delete(String(state));
|
pendingAuth.delete(String(state));
|
||||||
|
|
||||||
|
|
@ -163,9 +169,9 @@ router.get('/callback', async (req: Request, res: Response) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
res.redirect(settingsUrl(pending.redirectUri, 'connected'));
|
res.redirect(settingsUrl(pending.redirectUri, 'connected'));
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error('Etsy OAuth callback failed:', err);
|
console.error('Etsy OAuth callback failed:', err);
|
||||||
res.redirect(settingsUrl(pending.redirectUri, 'error'));
|
res.redirect(settingsUrl(pending.redirectUri, 'error', err?.message || 'Token exchange failed'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,10 +38,11 @@ interface TokenResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestToken = async (body: Record<string, string>): Promise<TokenResponse> => {
|
const requestToken = async (body: Record<string, string>): Promise<TokenResponse> => {
|
||||||
|
// OAuth 2.0 token endpoints take form-encoded bodies (RFC 6749)
|
||||||
const res = await fetch(ETSY_TOKEN_URL, {
|
const res = await fetch(ETSY_TOKEN_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
body: JSON.stringify(body),
|
body: new URLSearchParams(body).toString(),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error(`Etsy token request failed (${res.status}): ${await res.text()}`);
|
throw new Error(`Etsy token request failed (${res.status}): ${await res.text()}`);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue