From 1cf2903b8efd9e13f71b50e4e768f7930c1382eb Mon Sep 17 00:00:00 2001 From: Tautvydas Derzinskas Date: Mon, 19 Oct 2026 18:39:24 +0200 Subject: [PATCH] fix: correct category updates and improve NexPrint error handling --- backend/src/routes/categories.ts | 22 +++++++++++++------- backend/src/services/collectionSyncRunner.ts | 5 +++-- backend/src/services/nexprintApi.ts | 4 +++- backend/tests/nexprintApi.test.ts | 4 ++++ docs/features/importing.md | 4 ++-- extension/src/background/nexprintSession.ts | 5 ++++- 6 files changed, 30 insertions(+), 14 deletions(-) diff --git a/backend/src/routes/categories.ts b/backend/src/routes/categories.ts index 5fdebf1..270e31d 100644 --- a/backend/src/routes/categories.ts +++ b/backend/src/routes/categories.ts @@ -185,6 +185,12 @@ function parseCatIdsInput(raw: string | null | undefined): number[] { } const catIdsField = z.string().trim().max(1000).nullable().optional(); + +/** A field the request leaves out is left as it is (Prisma skips undefined), so a client from before a provider was + * added can't wipe that provider's ids; blank or null still clears them. */ +function catIdsUpdate(raw: string | null | undefined): number[] | undefined { + return raw === undefined ? undefined : parseCatIdsInput(raw); +} const categoryMetaSchema = z.object({ meta_title: z.string().trim().max(200).nullable().optional(), meta_description: z.string().trim().max(2000).nullable().optional(), @@ -209,14 +215,14 @@ router.patch( data: { metaTitle: body.meta_title || null, metaDescription: body.meta_description || null, - makerworldCatIds: parseCatIdsInput(body.makerworld_cat_ids), - thingiverseCatIds: parseCatIdsInput(body.thingiverse_cat_ids), - printablesCatIds: parseCatIdsInput(body.printables_cat_ids), - myminifactoryCatIds: parseCatIdsInput(body.myminifactory_cat_ids), - snapmakerCatIds: parseCatIdsInput(body.snapmaker_cat_ids), - makeronlineCatIds: parseCatIdsInput(body.makeronline_cat_ids), - crealitycloudCatIds: parseCatIdsInput(body.crealitycloud_cat_ids), - nexprintCatIds: parseCatIdsInput(body.nexprint_cat_ids).map(String), + makerworldCatIds: catIdsUpdate(body.makerworld_cat_ids), + thingiverseCatIds: catIdsUpdate(body.thingiverse_cat_ids), + printablesCatIds: catIdsUpdate(body.printables_cat_ids), + myminifactoryCatIds: catIdsUpdate(body.myminifactory_cat_ids), + snapmakerCatIds: catIdsUpdate(body.snapmaker_cat_ids), + makeronlineCatIds: catIdsUpdate(body.makeronline_cat_ids), + crealitycloudCatIds: catIdsUpdate(body.crealitycloud_cat_ids), + nexprintCatIds: catIdsUpdate(body.nexprint_cat_ids)?.map(String), }, }); res.json(toCategoryOut(updated)); diff --git a/backend/src/services/collectionSyncRunner.ts b/backend/src/services/collectionSyncRunner.ts index d2464ac..a206418 100644 --- a/backend/src/services/collectionSyncRunner.ts +++ b/backend/src/services/collectionSyncRunner.ts @@ -174,8 +174,9 @@ export async function fetchListing( return listing ? listing.entries.map((e) => e.modelId) : null; } if (sync.provider === "nexprint") { - // Collections, the private ones too, list without a sign-in; only their files need one. - const listing = await fetchNexprintCollection(id); + // Collections, the private ones too, list without a sign-in; only their files need one. All of it, not the import + // dialog's first 600, or models past those would never be found. + const listing = await fetchNexprintCollection(id, Number.POSITIVE_INFINITY); return listing ? listing.entries.map((e) => e.modelId) : null; } throw new Error(`Unknown provider "${sync.provider}"`); diff --git a/backend/src/services/nexprintApi.ts b/backend/src/services/nexprintApi.ts index aed31ee..35f8789 100644 --- a/backend/src/services/nexprintApi.ts +++ b/backend/src/services/nexprintApi.ts @@ -154,7 +154,9 @@ export async function checkNexprintAuth( try { const { code, data } = await fetchApi("/model-user-server/member/user_data", { body: {} }, token); const user = isRecord(data) && isRecord(data.userData) ? data.userData : null; - if (code !== CODE_OK || !user || !outId(user.userId, "U")) return code === null ? "unreachable" : "rejected"; + // Only a refusal says the token is bad; any other error code is NexPrint's own trouble. + if (code !== CODE_OK) return code === CODE_UNAUTHORIZED ? "rejected" : "unreachable"; + if (!user || !outId(user.userId, "U")) return "rejected"; return { userId: outId(user.userId, "U")!, name: str(user.nickname) }; } catch (err) { return err instanceof NexprintAuthError ? "rejected" : "unreachable"; diff --git a/backend/tests/nexprintApi.test.ts b/backend/tests/nexprintApi.test.ts index 5689235..7e33b9a 100644 --- a/backend/tests/nexprintApi.test.ts +++ b/backend/tests/nexprintApi.test.ts @@ -124,6 +124,10 @@ describe("NexPrint sign-in", () => { stubApi({ "/model-user-server/member/user_data": () => apiResponse(401, null, "账号未登录") }); expect(await checkNexprintAuth(TOKEN)).toBe("rejected"); + + // Its own trouble isn't the token's fault. + stubApi({ "/model-user-server/member/user_data": () => apiResponse(500, null, "系统繁忙") }); + expect(await checkNexprintAuth(TOKEN)).toBe("unreachable"); }); }); diff --git a/docs/features/importing.md b/docs/features/importing.md index ea58655..8649182 100644 --- a/docs/features/importing.md +++ b/docs/features/importing.md @@ -42,8 +42,8 @@ from Thingport Grab wait in **Administration → Import queue** until they're st Printables works with no setup. MakerWorld needs your session cookie, and Thingiverse and MyMiniFactory a key set by an admin; MyMiniFactory models you bought come in once you connect your MyMiniFactory account. Snapmaker Space needs your -own sign-in, which Thingport Grab keeps fresh, MakerOnline Thingport Grab for its files, and Creality Cloud and NexPrint your own -sign-in, which Grab sends on when you visit them. See +own sign-in, which Thingport Grab keeps fresh; MakerOnline's files come only through Thingport Grab; and Creality Cloud +and NexPrint need your own sign-in, which Grab sends to Thingport when you visit them. See [provider setup](../PROVIDER_SETUP.md). Admins can also [turn a provider off](../PROVIDER_SETUP.md#turning-a-provider-off) for the whole instance, and then nothing is imported from it. diff --git a/extension/src/background/nexprintSession.ts b/extension/src/background/nexprintSession.ts index c8054f2..8a48f87 100644 --- a/extension/src/background/nexprintSession.ts +++ b/extension/src/background/nexprintSession.ts @@ -20,7 +20,10 @@ export async function syncNexprintSession(): Promise { // Per Thingport account, so switching the extension to another one sends it there too. const account = `${config.instanceUrl} ${config.email}`; const last = config.lastSyncedNexprintSession; - const token = cookie?.value ? decodeURIComponent(cookie.value) : ""; + let token = ""; + try { + token = cookie?.value ? decodeURIComponent(cookie.value) : ""; + } catch {} // Malformed percent-encoding: not a token. // Signed out, the site clears the cookie or leaves a placeholder in it. if (!/^[0-9a-f]{32,128}$/i.test(token)) { // Clear the sign-in Grab sent, but not one pasted in Thingport.