From 9d07db9dc9c9cc3ef468ebdac96b9e2affe3132e Mon Sep 17 00:00:00 2001 From: Tautvydas Derzinskas Date: Sun, 13 Sep 2026 14:42:22 +0200 Subject: [PATCH] feat: make sure thingiverse token is verified --- backend/src/routes/settings.ts | 12 +++++++++++- backend/src/services/thingiverseApi.ts | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index 29a3623..cc42562 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -27,6 +27,7 @@ import { } from "../services/printService"; import { getUserMakerworldCookie, setUserMakerworldCookie } from "../services/makerworldCookieService"; import { verifyMakerworldCookie } from "../services/makerworldCloudApi"; +import { verifyThingiverseAccessToken } from "../services/thingiverseApi"; import { SLICER_IDS, getUserSlicer, setUserSlicer } from "../services/slicerPreferenceService"; import { THEME_SELECTIONS, getUserTheme, setUserTheme } from "../services/themePreferenceService"; import { checkForUpdates } from "../services/versionService"; @@ -143,8 +144,17 @@ router.post( requireAdmin, asyncHandler(async (req, res) => { const body = parseBody(thingiverseSettingsSchema, req.body); + const trimmed = (body.access_token ?? "").trim(); + // Only a new, non-empty token needs testing -- clearing it always succeeds, since there's + // nothing to verify a logged-out state against. + if (trimmed && !(await verifyThingiverseAccessToken(trimmed))) { + throw new HttpError( + 422, + "Couldn't verify this Thingiverse Access Token -- it may be invalid, revoked, or Thingiverse is rate-limiting this instance right now. Double-check the token at thingiverse.com/apps/create and try again.", + ); + } await setThingiverseAccessToken(body.access_token); - res.json({ configured: Boolean(body.access_token && body.access_token.trim()) }); + res.json({ configured: Boolean(trimmed) }); }), ); diff --git a/backend/src/services/thingiverseApi.ts b/backend/src/services/thingiverseApi.ts index ca5f6d9..3c8b5db 100644 --- a/backend/src/services/thingiverseApi.ts +++ b/backend/src/services/thingiverseApi.ts @@ -145,6 +145,23 @@ async function fetchThingiverseApiJson(path: string, accessToken: string): Promi return data; } +/** Called before storing an Access Token pasted into Admin Settings > Thingiverse (see + * routes/settings.ts' POST /settings/thingiverse), so a mistyped/revoked token is rejected up + * front instead of only surfacing as a failed import later. Reuses the same fetch path (and its + * FlareSolverr/rate-limit handling) as every other Thingiverse call here, against /users/me -- + * the official API's own "who am I" endpoint, so a success means the token is genuinely valid, + * not just well-formed. A transient Cloudflare rate-limit (ThingiverseRateLimitError) is treated + * the same as an invalid token here: either way this specific check can't confirm the token + * right now, and the caller's message covers both cases rather than guessing which one it was. */ +export async function verifyThingiverseAccessToken(accessToken: string): Promise { + try { + const data = await fetchThingiverseApiJson("/users/me", accessToken); + return isRecord(data); + } catch { + return false; + } +} + export type ThingiversePlateFile = { name: string; url: string }; export type ThingiverseGalleryImage = { name: string; url: string };