mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-18 20:59:55 -05:00
fix: show a proper error message when MakerWorld cookie fails due flareSolverr
This commit is contained in:
1 parent
46b8687d1d
commit
a81765e1ca
2 files changed
+40
-13
No files matched your search
@@ -28,7 +28,7 @@ import {
|
||||
validateStorageTemplate,
|
||||
} from "../services/printService";
|
||||
import { getUserMakerworldCookie, setUserMakerworldCookie } from "../services/makerworldCookieService";
|
||||
import { verifyMakerworldCookie } from "../services/makerworldCloudApi";
|
||||
import { type MakerworldCookieCheck, 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";
|
||||
@@ -282,6 +282,17 @@ router.get(
|
||||
// tab) and deliberately swallows failures, so adding a live network test there would only add
|
||||
// latency for no benefit. Only ProfilePage's manual Save (settingsApi.updateMakerworld) sends
|
||||
// `verify: true`.
|
||||
const MAKERWORLD_UNVERIFIABLE_MESSAGES: Record<
|
||||
Extract<MakerworldCookieCheck, { result: "unverifiable" }>["reason"],
|
||||
string
|
||||
> = {
|
||||
cloudflare_no_flaresolverr:
|
||||
"Couldn't check this cookie: MakerWorld's Cloudflare protection blocked the request and FlareSolverr isn't configured. Set FLARESOLVERR_URL and try again.",
|
||||
flaresolverr_failed:
|
||||
"Couldn't check this cookie: MakerWorld's Cloudflare protection blocked the request and FlareSolverr didn't get past it. Make sure FlareSolverr is running and reachable at FLARESOLVERR_URL, then try again.",
|
||||
network: "Couldn't check this cookie: MakerWorld didn't respond. Check this server's internet connection and try again.",
|
||||
};
|
||||
|
||||
const makerworldSettingsSchema = z.object({ cookie: z.string().nullable(), verify: z.boolean().optional() });
|
||||
router.patch(
|
||||
"/settings/makerworld",
|
||||
@@ -290,11 +301,16 @@ router.patch(
|
||||
const trimmed = (body.cookie ?? "").trim();
|
||||
// Only a new, non-empty cookie needs testing -- clearing it (trimmed === "") always
|
||||
// succeeds, since there's nothing to verify a logged-out state against.
|
||||
if (body.verify && trimmed && !(await verifyMakerworldCookie(trimmed))) {
|
||||
throw new HttpError(
|
||||
422,
|
||||
"Couldn't verify this MakerWorld cookie -- it may be invalid or expired. Copy a fresh Cookie header from a logged-in makerworld.com tab and try again.",
|
||||
);
|
||||
if (body.verify && trimmed) {
|
||||
const check = await verifyMakerworldCookie(trimmed);
|
||||
if (check.result === "invalid") {
|
||||
throw new HttpError(
|
||||
422,
|
||||
"MakerWorld rejected this cookie -- it may be invalid or expired. Copy a fresh Cookie header from a logged-in makerworld.com tab and try again.",
|
||||
);
|
||||
}
|
||||
// 503, not 422: nothing is known to be wrong with the cookie, the check itself couldn't run.
|
||||
if (check.result === "unverifiable") throw new HttpError(503, MAKERWORLD_UNVERIFIABLE_MESSAGES[check.reason]);
|
||||
}
|
||||
const configured = await setUserMakerworldCookie(req.userId!, body.cookie);
|
||||
res.json({ configured });
|
||||
|
||||
@@ -101,15 +101,23 @@ export function extractMakerworldBearerToken(rawCookieOrToken: string | null | u
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Outcome of verifyMakerworldCookie. "unverifiable" means the check never got an answer about
|
||||
* the cookie itself -- Cloudflare challenged the request and FlareSolverr is unset, unreachable,
|
||||
* or couldn't solve it, or MakerWorld didn't respond -- so the cookie may be perfectly fine. */
|
||||
export type MakerworldCookieCheck =
|
||||
| { result: "valid" }
|
||||
| { result: "invalid" }
|
||||
| { result: "unverifiable"; reason: "cloudflare_no_flaresolverr" | "flaresolverr_failed" | "network" };
|
||||
|
||||
/** Called before storing a cookie pasted into Profile > MakerWorld (see routes/settings.ts'
|
||||
* PATCH /settings/makerworld), so a stale/expired/mistyped paste is rejected up front instead of
|
||||
* only surfacing as a failed import later. A malformed paste (extractMakerworldBearerToken
|
||||
* finding no usable token at all) is rejected without a network call; otherwise this is the
|
||||
* same self-profile request api.bambulab.com/MakerWorld clients use right after login, so a 200
|
||||
* here means the account is genuinely logged in, not just that the string looks token-shaped. */
|
||||
export async function verifyMakerworldCookie(rawCookieOrToken: string): Promise<boolean> {
|
||||
export async function verifyMakerworldCookie(rawCookieOrToken: string): Promise<MakerworldCookieCheck> {
|
||||
const bearerToken = extractMakerworldBearerToken(rawCookieOrToken);
|
||||
if (!bearerToken) return false;
|
||||
if (!bearerToken) return { result: "invalid" };
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), CLOUD_API_TIMEOUT_MS);
|
||||
try {
|
||||
@@ -123,18 +131,21 @@ export async function verifyMakerworldCookie(rawCookieOrToken: string): Promise<
|
||||
redirect: "follow",
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (res.status === 403 && isFlaresolverrEnabled() && looksLikeCloudflareBlock(res.headers)) {
|
||||
if (res.status === 403 && looksLikeCloudflareBlock(res.headers)) {
|
||||
// A Cloudflare challenge is about this server's IP, not the token -- it says nothing
|
||||
// either way about the cookie, so without a way past it the answer is "couldn't check".
|
||||
if (!isFlaresolverrEnabled()) return { result: "unverifiable", reason: "cloudflare_no_flaresolverr" };
|
||||
// This endpoint answers 200 with an empty body through FlareSolverr's browser when the
|
||||
// session isn't actually logged in, so a bare status check would accept any string here.
|
||||
// Assert on the parsed body instead -- only a genuinely authenticated session has a `uid`.
|
||||
const solved = await fetchViaFlaresolverr(SELF_PREFERENCE_URL, `token=${bearerToken}`);
|
||||
if (!solved) return false;
|
||||
if (!solved) return { result: "unverifiable", reason: "flaresolverr_failed" };
|
||||
const data = extractJsonFromBrowserBody(solved.body);
|
||||
return isRecord(data) && data.uid != null;
|
||||
return isRecord(data) && data.uid != null ? { result: "valid" } : { result: "invalid" };
|
||||
}
|
||||
return res.ok;
|
||||
return res.ok ? { result: "valid" } : { result: "invalid" };
|
||||
} catch {
|
||||
return false;
|
||||
return { result: "unverifiable", reason: "network" };
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user