diff --git a/typescript/server/src/server/router/api/v1/import/router.test.ts b/typescript/server/src/server/router/api/v1/import/router.test.ts index 8af962d7e..a83e1c35d 100644 --- a/typescript/server/src/server/router/api/v1/import/router.test.ts +++ b/typescript/server/src/server/router/api/v1/import/router.test.ts @@ -6,6 +6,52 @@ import { afterAll, describe, expect, it } from "vitest"; afterAll(() => CloseServerConnection()); +describe("POST /api/v1/import/from-api", () => { + it("returns 400 when the import type is not enabled on this instance", async () => { + const { id: userId } = await seedUser({ + username: "from_api_disabled_type", + withCredential: true, + withSettings: true, + }); + await seedApiToken({ + token: "from_api_disabled", + userId, + submitScore: true, + }); + + const res = await mockApi + .post("/api/v1/import/from-api") + .set("Authorization", "Bearer from_api_disabled") + .send({ importType: "api/cg-dev-sdvx" }); + + expect(res.status).toBe(400); + expect(res.body.success).toBe(false); + expect(String(res.body.description)).toMatch(/not enabled on this instance/iu); + }); + + it("returns 400 for non-API import types", async () => { + const { id: userId } = await seedUser({ + username: "from_api_wrong_type", + withCredential: true, + withSettings: true, + }); + await seedApiToken({ + token: "from_api_wrong", + userId, + submitScore: true, + }); + + const res = await mockApi + .post("/api/v1/import/from-api") + .set("Authorization", "Bearer from_api_wrong") + .send({ importType: "file/batch-manual" }); + + expect(res.status).toBe(400); + expect(res.body.success).toBe(false); + expect(String(res.body.description)).toMatch(/Invalid import type/iu); + }); +}); + describe("import/orphans auth", () => { it("returns 403 for unauthenticated GET list", async () => { const res = await mockApi.get("/api/v1/import/orphans"); diff --git a/typescript/server/src/server/router/api/v1/import/router.ts b/typescript/server/src/server/router/api/v1/import/router.ts index f56fe2df4..8d6d33a70 100644 --- a/typescript/server/src/server/router/api/v1/import/router.ts +++ b/typescript/server/src/server/router/api/v1/import/router.ts @@ -20,7 +20,8 @@ import { Random20Hex } from "#utils/misc"; import { FormatUserDoc, GetUserWithIDGuaranteed } from "#utils/user"; import { ExpectedErr } from "bliss"; import { p } from "prudence"; -import { type FileUploadImportTypes, type V3Game } from "tachi-common"; +import { type APIImportTypes, type FileUploadImportTypes, type V3Game } from "tachi-common"; +import { apiImportTypes } from "tachi-common/constants/import-types"; import { API_V1_ROUTER } from "../_singleton"; @@ -28,6 +29,22 @@ const ParseMultipartScoredata = CreateMulterSingleUploadMiddleware("scoreData", const fileImportTypes = TachiConfig.IMPORT_TYPES.filter((e) => e.startsWith("file/")); +const enabledApiImportTypes = TachiConfig.IMPORT_TYPES.filter((e): e is APIImportTypes => + e.startsWith("api/"), +); + +function assertEnabledApiImportType(importType: string): APIImportTypes { + if (!apiImportTypes.includes(importType as APIImportTypes)) { + throw new ExpectedErr(400, `Invalid import type: ${importType}`); + } + + if (!enabledApiImportTypes.includes(importType as APIImportTypes)) { + throw new ExpectedErr(400, `Import type "${importType}" is not enabled on this instance.`); + } + + return importType as APIImportTypes; +} + /** * Import scores from a file. Expects the post request to be multipart, * and to provide a scoreData file. @@ -91,8 +108,7 @@ API_V1_ROUTER.rawAdd( * @name POST /api/v1/import/from-api */ API_V1_ROUTER.add("POST /import/from-api", async ({ input, req }) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const importType = input.importType as any; + const importType = assertEnabledApiImportType(input.importType); const importID = Random20Hex(); const userID = req[SYMBOL_TACHI_API_AUTH].userID!; const userIntent = req.header("X-User-Intent")?.toLowerCase() === "true";