From 6442bd0ca5c4a158771989fd6d417380a1230cdc Mon Sep 17 00:00:00 2001 From: zkldi Date: Sat, 4 Sep 2021 02:25:03 +0100 Subject: [PATCH 1/6] Add neater prefixes to clientID and clientSecret --- server/src/server/router/api/v1/oauth/clients/router.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/server/router/api/v1/oauth/clients/router.ts b/server/src/server/router/api/v1/oauth/clients/router.ts index 2b7168a99..45a73c80f 100644 --- a/server/src/server/router/api/v1/oauth/clients/router.ts +++ b/server/src/server/router/api/v1/oauth/clients/router.ts @@ -87,8 +87,8 @@ router.post( }); } - const clientID = Random20Hex(); - const clientSecret = Random20Hex(); + const clientID = `CI${Random20Hex()}`; + const clientSecret = `CS${Random20Hex()}`; const clientDoc = { clientID, From 7360e61386acfe2d6c421326a9f099af9449e7de Mon Sep 17 00:00:00 2001 From: zkldi Date: Sat, 4 Sep 2021 02:31:32 +0100 Subject: [PATCH 2/6] Fervidex Card Management --- server/package.json | 2 +- server/pnpm-lock.yaml | 8 +- server/src/external/mongo/db.ts | 3 + server/src/external/mongo/indexes.ts | 1 + .../integrations/fervidex/router.test.ts | 142 ++++++++++++++++++ .../_userID/integrations/fervidex/router.ts | 79 ++++++++++ .../v1/users/_userID/integrations/router.ts | 2 + server/src/test-utils/mock-db/fer-cards.json | 6 + 8 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts create mode 100644 server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts create mode 100644 server/src/test-utils/mock-db/fer-cards.json diff --git a/server/package.json b/server/package.json index 9c330485a..9c03f79af 100644 --- a/server/package.json +++ b/server/package.json @@ -71,7 +71,7 @@ "redis": "3.1.2", "rimraf": "3.0.2", "safe-json-stringify": "1.2.0", - "tachi-common": "0.1.50", + "tachi-common": "0.1.52", "typescript": "4.3.4", "winston": "3.3.3" }, diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index 762390c4b..d98a6d95c 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -52,7 +52,7 @@ specifiers: rimraf: 3.0.2 safe-json-stringify: 1.2.0 supertest: 6.1.3 - tachi-common: 0.1.50 + tachi-common: 0.1.52 tap: 15.0.9 ts-node: 10.0.0 tsconfig-paths: 3.10.1 @@ -85,7 +85,7 @@ dependencies: redis: 3.1.2 rimraf: 3.0.2 safe-json-stringify: 1.2.0 - tachi-common: 0.1.50_ts-node@10.0.0+typescript@4.3.4 + tachi-common: 0.1.52_ts-node@10.0.0+typescript@4.3.4 typescript: 4.3.4 winston: 3.3.3 @@ -3666,8 +3666,8 @@ packages: strip-ansi: 6.0.0 dev: true - /tachi-common/0.1.50_ts-node@10.0.0+typescript@4.3.4: - resolution: {integrity: sha512-F84jmrubIuhUSUZx7sm8aDpm43vklEgXT2j3XMaQoLLuuKIz5BgehMgNNNkJYoeRPvyEj/2Q/Bw46ye+D0q6Dg==} + /tachi-common/0.1.52_ts-node@10.0.0+typescript@4.3.4: + resolution: {integrity: sha512-7QJw2r9Yb6DYWsLbDPFa4R73/LxVAsNXHsN2k4uDiI31EdYHzmQDRSbvwQcw6jEraOojRD9l5aLAemg/uYcm7g==} dependencies: monk: 7.3.4 tap: 15.0.9_ts-node@10.0.0+typescript@4.3.4 diff --git a/server/src/external/mongo/db.ts b/server/src/external/mongo/db.ts index 44486a5a2..76c456a09 100644 --- a/server/src/external/mongo/db.ts +++ b/server/src/external/mongo/db.ts @@ -33,6 +33,7 @@ import { PublicUserDocument, OAuth2ApplicationDocument, integer, + FervidexCardsDocument, } from "tachi-common"; import monk, { TMiddleware } from "monk"; import CreateLogCtx from "lib/logger/logger"; @@ -175,6 +176,7 @@ const db = { "oauth2-auth-codes": // i've inlined this one because i don't see it appearing anywhere else. monkDB.get<{ code: string; userID: integer; createdOn: number }>("oauth2-auth-codes"), + "fer-cards": monkDB.get("fer-cards"), }; export type StaticDatabases = @@ -205,6 +207,7 @@ export type StaticDatabases = | "user-private-information" | "oauth2-clients" | "oauth2-auth-codes" + | "fer-cards" | "user-settings"; export type Databases = StaticDatabases | `songs-${Game}` | `charts-${Game}`; diff --git a/server/src/external/mongo/indexes.ts b/server/src/external/mongo/indexes.ts index 9b9946bf9..eb71bd3f8 100644 --- a/server/src/external/mongo/indexes.ts +++ b/server/src/external/mongo/indexes.ts @@ -91,6 +91,7 @@ const staticIndexes: Partial> = { ], "user-settings": [index({ userID: 1 }, UNIQUE)], "user-private-information": [index({ userID: 1 }, UNIQUE)], + "fer-cards": [index({ userID: 1 }, UNIQUE)], }; const indexes: Partial> = staticIndexes; diff --git a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts new file mode 100644 index 000000000..063ab69e3 --- /dev/null +++ b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts @@ -0,0 +1,142 @@ +import db from "external/mongo/db"; +import { PublicUserDocument } from "tachi-common"; +import t from "tap"; +import { CreateFakeAuthCookie } from "test-utils/fake-auth"; +import mockApi from "test-utils/mock-api"; +import ResetDBState from "test-utils/resets"; + +t.test("GET /api/v1/users/:userID/integrations/fervidex/card", async (t) => { + t.beforeEach(ResetDBState); + + const cookie = await CreateFakeAuthCookie(mockApi); + + t.test("Should return null if this user has cards set to null.", async (t) => { + const res = await mockApi + .get("/api/v1/users/1/integrations/fervidex/cards") + .set("Cookie", cookie); + + t.equal(res.statusCode, 200); + + t.equal(res.body.body, null); + + t.end(); + }); + + t.test("Should return null if this user has no cards set.", async (t) => { + await db["fer-cards"].remove({}); + + const res = await mockApi + .get("/api/v1/users/1/integrations/fervidex/cards") + .set("Cookie", cookie); + + t.equal(res.statusCode, 200); + + t.equal(res.body.body, null); + + t.end(); + }); + + t.test("Should return this users list of cards if they have some set.", async (t) => { + await db["fer-cards"].update({ userID: 1 }, { $set: { cards: ["foo", "bar"] } }); + + const res = await mockApi + .get("/api/v1/users/1/integrations/fervidex/cards") + .set("Cookie", cookie); + + t.equal(res.statusCode, 200); + + t.strictSame(res.body.body, ["foo", "bar"]); + + t.end(); + }); + + t.test("Must require self-key level authentication.", async (t) => { + const res = await mockApi.get("/api/v1/users/1/integrations/fervidex/cards"); + + t.equal(res.statusCode, 401); + + const res2 = await mockApi + .get("/api/v1/users/1/integrations/fervidex/cards") + .set("Authorization", "Bearer fake_api_token"); + + t.equal(res2.statusCode, 403); + + // insert a fake user doc so this doesn't 404 + await db.users.insert({ + id: 2, + username: "foo", + usernameLowercase: "foo", + } as PublicUserDocument); + + const res3 = await mockApi + .get("/api/v1/users/2/integrations/fervidex/cards") + .set("Cookie", cookie); + + t.equal(res3.statusCode, 403); + + t.end(); + }); + + t.end(); +}); + +t.test("PUT /api/v1/users/:userID/integrations/fervidex/cards", async (t) => { + t.beforeEach(ResetDBState); + + const cookie = await CreateFakeAuthCookie(mockApi); + + t.test("Should update a users cards.", async (t) => { + const res = await mockApi + .put("/api/v1/users/1/integrations/fervidex/cards") + .send({ + cards: ["foo", "bar"], + }) + .set("Cookie", cookie); + + t.strictSame(res.body.body, ["foo", "bar"]); + + const dbRes = await db["fer-cards"].findOne({ userID: 1 }); + + t.strictSame(dbRes?.cards, ["foo", "bar"]); + + t.end(); + }); + + t.test("Should insert a card filter document if one doesn't exist.", async (t) => { + await db["fer-cards"].remove({}); + + const res = await mockApi + .put("/api/v1/users/1/integrations/fervidex/cards") + .send({ + cards: ["foo", "bar"], + }) + .set("Cookie", cookie); + + t.strictSame(res.body.body, ["foo", "bar"]); + + const dbRes = await db["fer-cards"].findOne({ userID: 1 }); + + t.strictSame(dbRes?.cards, ["foo", "bar"]); + + t.end(); + }); + + t.test("Should null cards if null is provided.", async (t) => { + const res = await mockApi + .put("/api/v1/users/1/integrations/fervidex/cards") + .send({ + cards: null, + }) + .set("Cookie", cookie); + + t.strictSame(res.body.body, null); + + const dbRes = await db["fer-cards"].findOne({ userID: 1 }); + + t.strictSame(dbRes?.cards, null); + + t.end(); + }); + + t.end(); +}); diff --git a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts new file mode 100644 index 000000000..8235b1b2b --- /dev/null +++ b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts @@ -0,0 +1,79 @@ +import { Router } from "express"; +import p from "prudence"; +import db from "external/mongo/db"; +import { SYMBOL_TachiData } from "lib/constants/tachi"; +import prValidate from "server/middleware/prudence-validate"; +import { RequireKamaitachi } from "server/middleware/type-require"; +import { RequireSelfRequestFromUser } from "../../middleware"; + +const router: Router = Router({ mergeParams: true }); + +router.use(RequireKamaitachi); +router.use(RequireSelfRequestFromUser); + +/** + * Retrieve all of your configured fervidex cards. Returns null instead + * of an empty array if no cards are configured. + * + * @name GET /api/v1/users/:userID/integrations/fervidex/cards + */ +router.get("/cards", async (req, res) => { + const user = req[SYMBOL_TachiData]!.requestedUser!; + + const cardDoc = await db["fer-cards"].findOne({ + userID: user.id, + }); + + if (!cardDoc || !cardDoc.cards) { + return res.status(200).json({ + success: true, + description: `No card filters enabled.`, + body: null, + }); + } + + return res.status(200).json({ + success: true, + description: `Found ${cardDoc.cards.length} card filters.`, + body: cardDoc.cards, + }); +}); + +/** + * Replace your configured fervidex cards. Alternatively, pass null to disable + * the filter. + * + * @param cards - An array of strings or null. + * + * @name PUT /api/v1/users/:userID/integrations/fervidex + */ +router.put("/cards", prValidate({ cards: p.nullable(["string"]) }), async (req, res) => { + if (req.body.cards && req.body.cards.length > 6) { + return res.status(400).json({ + success: false, + description: `You cannot have more than 6 card filters at once.`, + }); + } + + const user = req[SYMBOL_TachiData]!.requestedUser!; + + await db["fer-cards"].update( + { userID: user.id }, + { + $set: { + cards: req.body.cards, + }, + }, + { + upsert: true, + } + ); + + return res.status(200).json({ + success: true, + description: `Successfully updated cards.`, + body: req.body.cards, + }); +}); + +export default router; diff --git a/server/src/server/router/api/v1/users/_userID/integrations/router.ts b/server/src/server/router/api/v1/users/_userID/integrations/router.ts index 918ea9484..bbcf46819 100644 --- a/server/src/server/router/api/v1/users/_userID/integrations/router.ts +++ b/server/src/server/router/api/v1/users/_userID/integrations/router.ts @@ -1,4 +1,5 @@ import { Router } from "express"; +import fervidexRouter from "./fervidex/router"; import arcRouter from "./arc/router"; import kaiKaiTypeRouter from "./kai/_kaiType/router"; @@ -6,5 +7,6 @@ const router: Router = Router({ mergeParams: true }); router.use("/arc", arcRouter); router.use("/kai/:kaiType", kaiKaiTypeRouter); +router.use("/fervidex", fervidexRouter); export default router; diff --git a/server/src/test-utils/mock-db/fer-cards.json b/server/src/test-utils/mock-db/fer-cards.json new file mode 100644 index 000000000..6a89671de --- /dev/null +++ b/server/src/test-utils/mock-db/fer-cards.json @@ -0,0 +1,6 @@ +[ + { + "userID": 1, + "cards": null + } +] \ No newline at end of file From 9f69089151f82e84947d3a6fedd586cc633dd15e Mon Sep 17 00:00:00 2001 From: zkldi Date: Sat, 4 Sep 2021 02:39:33 +0100 Subject: [PATCH 3/6] Add card filtering on the fervidex endpoints. --- .../server/router/ir/fervidex/router.test.ts | 31 ++++++++++++++++ .../src/server/router/ir/fervidex/router.ts | 37 ++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/server/src/server/router/ir/fervidex/router.test.ts b/server/src/server/router/ir/fervidex/router.test.ts index 335f6ef4f..f6befe895 100644 --- a/server/src/server/router/ir/fervidex/router.test.ts +++ b/server/src/server/router/ir/fervidex/router.test.ts @@ -8,6 +8,37 @@ import { GetKTDataJSON } from "test-utils/test-data"; // eslint-disable-next-line @typescript-eslint/no-explicit-any function TestHeaders(url: string, data: any) { + t.test("Should validate against card filters", async (t) => { + await db["fer-cards"].remove({}); + await db["fer-cards"].insert({ + userID: 1, + cards: ["foo"], + }); + + const res = await mockApi + .post(url) + .set("Authorization", "Bearer mock_token") + // rootage + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .set("X-Account-Id", "bar") + .set("User-Agent", "fervidex/1.3.0") + .send(data); + + t.equal(res.body.success, false, "Should reject invalid card."); + + const res2 = await mockApi + .post(url) + .set("Authorization", "Bearer mock_token") + // rootage + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .set("User-Agent", "fervidex/1.3.0") + .send(data); + + t.equal(res2.body.success, false, "Should reject no card."); + + t.end(); + }); + t.test("Should reject invalid X-Software-Models", async (t) => { let res = await mockApi .post(url) diff --git a/server/src/server/router/ir/fervidex/router.ts b/server/src/server/router/ir/fervidex/router.ts index 624a2dc4b..06ad60fe9 100644 --- a/server/src/server/router/ir/fervidex/router.ts +++ b/server/src/server/router/ir/fervidex/router.ts @@ -8,8 +8,9 @@ import { ParseFervidexStatic } from "lib/score-import/import-types/ir/fervidex-s import { ParseFervidexSingle } from "lib/score-import/import-types/ir/fervidex/parser"; import { Playtypes, integer } from "tachi-common"; import CreateLogCtx from "lib/logger/logger"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "lib/constants/tachi"; import { RequirePermissions } from "server/middleware/auth"; +import db from "external/mongo/db"; const logger = CreateLogCtx(__filename); @@ -150,7 +151,39 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => { return next(); }; -router.use(RequirePermissions("submit_score"), ValidateFervidexHeader, ValidateModelHeader); +const ValidateCards: RequestHandler = async (req, res, next) => { + const userID = req[SYMBOL_TachiAPIAuth]!.userID!; + + const cardFilters = await db["fer-cards"].findOne({ userID }); + + if (!cardFilters || !cardFilters.cards) { + return next(); + } + + const cardID = req.header("X-Account-Id"); + if (!cardID) { + return res.status(400).json({ + success: false, + description: `Fervidex did not provide a card ID.`, + }); + } + + if (!cardFilters.cards.includes(cardID)) { + return res.status(400).json({ + success: false, + description: `The card ID ${cardID} is not in your list of filters. Ignoring.`, + }); + } + + return next(); +}; + +router.use( + RequirePermissions("submit_score"), + ValidateFervidexHeader, + ValidateModelHeader, + ValidateCards +); /** * Submits all of a users data to Kamaitachi. This data is extremely minimal, From e70e076525b02ed7356963c220719b3d6a2fadc8 Mon Sep 17 00:00:00 2001 From: zkldi Date: Sat, 4 Sep 2021 02:47:39 +0100 Subject: [PATCH 4/6] Change all instances of fer-cards to fer-settings --- server/package.json | 2 +- server/src/external/mongo/db.ts | 4 ++-- server/src/external/mongo/indexes.ts | 2 +- .../_userID/integrations/fervidex/router.test.ts | 12 ++++++------ .../v1/users/_userID/integrations/fervidex/router.ts | 4 ++-- server/src/server/router/ir/fervidex/router.test.ts | 4 ++-- server/src/server/router/ir/fervidex/router.ts | 2 +- server/src/test-utils/mock-db/fer-cards.json | 6 ------ server/src/test-utils/mock-db/fer-settings.json | 7 +++++++ 9 files changed, 22 insertions(+), 21 deletions(-) delete mode 100644 server/src/test-utils/mock-db/fer-cards.json create mode 100644 server/src/test-utils/mock-db/fer-settings.json diff --git a/server/package.json b/server/package.json index 9c03f79af..8cf67ddbb 100644 --- a/server/package.json +++ b/server/package.json @@ -71,7 +71,7 @@ "redis": "3.1.2", "rimraf": "3.0.2", "safe-json-stringify": "1.2.0", - "tachi-common": "0.1.52", + "tachi-common": "0.1.53", "typescript": "4.3.4", "winston": "3.3.3" }, diff --git a/server/src/external/mongo/db.ts b/server/src/external/mongo/db.ts index 76c456a09..445e5a0da 100644 --- a/server/src/external/mongo/db.ts +++ b/server/src/external/mongo/db.ts @@ -176,7 +176,7 @@ const db = { "oauth2-auth-codes": // i've inlined this one because i don't see it appearing anywhere else. monkDB.get<{ code: string; userID: integer; createdOn: number }>("oauth2-auth-codes"), - "fer-cards": monkDB.get("fer-cards"), + "fer-settings": monkDB.get("fer-settings"), }; export type StaticDatabases = @@ -207,7 +207,7 @@ export type StaticDatabases = | "user-private-information" | "oauth2-clients" | "oauth2-auth-codes" - | "fer-cards" + | "fer-settings" | "user-settings"; export type Databases = StaticDatabases | `songs-${Game}` | `charts-${Game}`; diff --git a/server/src/external/mongo/indexes.ts b/server/src/external/mongo/indexes.ts index eb71bd3f8..342c0f557 100644 --- a/server/src/external/mongo/indexes.ts +++ b/server/src/external/mongo/indexes.ts @@ -91,7 +91,7 @@ const staticIndexes: Partial> = { ], "user-settings": [index({ userID: 1 }, UNIQUE)], "user-private-information": [index({ userID: 1 }, UNIQUE)], - "fer-cards": [index({ userID: 1 }, UNIQUE)], + "fer-settings": [index({ userID: 1 }, UNIQUE)], }; const indexes: Partial> = staticIndexes; diff --git a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts index 063ab69e3..38954d1ca 100644 --- a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts @@ -23,7 +23,7 @@ t.test("GET /api/v1/users/:userID/integrations/fervidex/card", async (t) => { }); t.test("Should return null if this user has no cards set.", async (t) => { - await db["fer-cards"].remove({}); + await db["fer-settings"].remove({}); const res = await mockApi .get("/api/v1/users/1/integrations/fervidex/cards") @@ -37,7 +37,7 @@ t.test("GET /api/v1/users/:userID/integrations/fervidex/card", async (t) => { }); t.test("Should return this users list of cards if they have some set.", async (t) => { - await db["fer-cards"].update({ userID: 1 }, { $set: { cards: ["foo", "bar"] } }); + await db["fer-settings"].update({ userID: 1 }, { $set: { cards: ["foo", "bar"] } }); const res = await mockApi .get("/api/v1/users/1/integrations/fervidex/cards") @@ -95,7 +95,7 @@ t.test("PUT /api/v1/users/:userID/integrations/fervidex/cards", async (t) => { t.strictSame(res.body.body, ["foo", "bar"]); - const dbRes = await db["fer-cards"].findOne({ userID: 1 }); + const dbRes = await db["fer-settings"].findOne({ userID: 1 }); t.strictSame(dbRes?.cards, ["foo", "bar"]); @@ -103,7 +103,7 @@ t.test("PUT /api/v1/users/:userID/integrations/fervidex/cards", async (t) => { }); t.test("Should insert a card filter document if one doesn't exist.", async (t) => { - await db["fer-cards"].remove({}); + await db["fer-settings"].remove({}); const res = await mockApi .put("/api/v1/users/1/integrations/fervidex/cards") @@ -114,7 +114,7 @@ t.test("PUT /api/v1/users/:userID/integrations/fervidex/cards", async (t) => { t.strictSame(res.body.body, ["foo", "bar"]); - const dbRes = await db["fer-cards"].findOne({ userID: 1 }); + const dbRes = await db["fer-settings"].findOne({ userID: 1 }); t.strictSame(dbRes?.cards, ["foo", "bar"]); @@ -131,7 +131,7 @@ t.test("PUT /api/v1/users/:userID/integrations/fervidex/cards", async (t) => { t.strictSame(res.body.body, null); - const dbRes = await db["fer-cards"].findOne({ userID: 1 }); + const dbRes = await db["fer-settings"].findOne({ userID: 1 }); t.strictSame(dbRes?.cards, null); diff --git a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts index 8235b1b2b..38e4fe29a 100644 --- a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts +++ b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts @@ -20,7 +20,7 @@ router.use(RequireSelfRequestFromUser); router.get("/cards", async (req, res) => { const user = req[SYMBOL_TachiData]!.requestedUser!; - const cardDoc = await db["fer-cards"].findOne({ + const cardDoc = await db["fer-settings"].findOne({ userID: user.id, }); @@ -57,7 +57,7 @@ router.put("/cards", prValidate({ cards: p.nullable(["string"]) }), async (req, const user = req[SYMBOL_TachiData]!.requestedUser!; - await db["fer-cards"].update( + await db["fer-settings"].update( { userID: user.id }, { $set: { diff --git a/server/src/server/router/ir/fervidex/router.test.ts b/server/src/server/router/ir/fervidex/router.test.ts index f6befe895..801a8c0bf 100644 --- a/server/src/server/router/ir/fervidex/router.test.ts +++ b/server/src/server/router/ir/fervidex/router.test.ts @@ -9,8 +9,8 @@ import { GetKTDataJSON } from "test-utils/test-data"; // eslint-disable-next-line @typescript-eslint/no-explicit-any function TestHeaders(url: string, data: any) { t.test("Should validate against card filters", async (t) => { - await db["fer-cards"].remove({}); - await db["fer-cards"].insert({ + await db["fer-settings"].remove({}); + await db["fer-settings"].insert({ userID: 1, cards: ["foo"], }); diff --git a/server/src/server/router/ir/fervidex/router.ts b/server/src/server/router/ir/fervidex/router.ts index 06ad60fe9..2daa611b6 100644 --- a/server/src/server/router/ir/fervidex/router.ts +++ b/server/src/server/router/ir/fervidex/router.ts @@ -154,7 +154,7 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => { const ValidateCards: RequestHandler = async (req, res, next) => { const userID = req[SYMBOL_TachiAPIAuth]!.userID!; - const cardFilters = await db["fer-cards"].findOne({ userID }); + const cardFilters = await db["fer-settings"].findOne({ userID }); if (!cardFilters || !cardFilters.cards) { return next(); diff --git a/server/src/test-utils/mock-db/fer-cards.json b/server/src/test-utils/mock-db/fer-cards.json deleted file mode 100644 index 6a89671de..000000000 --- a/server/src/test-utils/mock-db/fer-cards.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "userID": 1, - "cards": null - } -] \ No newline at end of file diff --git a/server/src/test-utils/mock-db/fer-settings.json b/server/src/test-utils/mock-db/fer-settings.json new file mode 100644 index 000000000..6afc986a5 --- /dev/null +++ b/server/src/test-utils/mock-db/fer-settings.json @@ -0,0 +1,7 @@ +[ + { + "userID": 1, + "cards": null, + "forceStaticImport": false + } +] \ No newline at end of file From 075ecef6f3d918b1e447cf24e864fef20611db6f Mon Sep 17 00:00:00 2001 From: zkldi Date: Sat, 4 Sep 2021 03:56:18 +0100 Subject: [PATCH 5/6] Change endpoints to control settings instead of cards --- .../integrations/fervidex/router.test.ts | 54 +++++----- .../_userID/integrations/fervidex/router.ts | 99 ++++++++++--------- 2 files changed, 76 insertions(+), 77 deletions(-) diff --git a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts index 38954d1ca..fc5db6b54 100644 --- a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts @@ -5,28 +5,16 @@ import { CreateFakeAuthCookie } from "test-utils/fake-auth"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; -t.test("GET /api/v1/users/:userID/integrations/fervidex/card", async (t) => { +t.test("GET /api/v1/users/:userID/integrations/fervidex/s", async (t) => { t.beforeEach(ResetDBState); const cookie = await CreateFakeAuthCookie(mockApi); - t.test("Should return null if this user has cards set to null.", async (t) => { - const res = await mockApi - .get("/api/v1/users/1/integrations/fervidex/cards") - .set("Cookie", cookie); - - t.equal(res.statusCode, 200); - - t.equal(res.body.body, null); - - t.end(); - }); - - t.test("Should return null if this user has no cards set.", async (t) => { + t.test("Should return null if this user has no settings set.", async (t) => { await db["fer-settings"].remove({}); const res = await mockApi - .get("/api/v1/users/1/integrations/fervidex/cards") + .get("/api/v1/users/1/integrations/fervidex/settings") .set("Cookie", cookie); t.equal(res.statusCode, 200); @@ -36,27 +24,31 @@ t.test("GET /api/v1/users/:userID/integrations/fervidex/card", async (t) => { t.end(); }); - t.test("Should return this users list of cards if they have some set.", async (t) => { + t.test("Should return this users settings if they have them.", async (t) => { await db["fer-settings"].update({ userID: 1 }, { $set: { cards: ["foo", "bar"] } }); const res = await mockApi - .get("/api/v1/users/1/integrations/fervidex/cards") + .get("/api/v1/users/1/integrations/fervidex/settings") .set("Cookie", cookie); t.equal(res.statusCode, 200); - t.strictSame(res.body.body, ["foo", "bar"]); + t.strictSame(res.body.body, { + userID: 1, + forceStaticImport: false, + cards: ["foo", "bar"], + }); t.end(); }); t.test("Must require self-key level authentication.", async (t) => { - const res = await mockApi.get("/api/v1/users/1/integrations/fervidex/cards"); + const res = await mockApi.get("/api/v1/users/1/integrations/fervidex/settings"); t.equal(res.statusCode, 401); const res2 = await mockApi - .get("/api/v1/users/1/integrations/fervidex/cards") + .get("/api/v1/users/1/integrations/fervidex/settings") .set("Authorization", "Bearer fake_api_token"); t.equal(res2.statusCode, 403); @@ -69,7 +61,7 @@ t.test("GET /api/v1/users/:userID/integrations/fervidex/card", async (t) => { } as PublicUserDocument); const res3 = await mockApi - .get("/api/v1/users/2/integrations/fervidex/cards") + .get("/api/v1/users/2/integrations/fervidex/settings") .set("Cookie", cookie); t.equal(res3.statusCode, 403); @@ -80,20 +72,20 @@ t.test("GET /api/v1/users/:userID/integrations/fervidex/card", async (t) => { t.end(); }); -t.test("PUT /api/v1/users/:userID/integrations/fervidex/cards", async (t) => { +t.test("PATCH /api/v1/users/:userID/integrations/fervidex/settings", async (t) => { t.beforeEach(ResetDBState); const cookie = await CreateFakeAuthCookie(mockApi); - t.test("Should update a users cards.", async (t) => { + t.test("Should update a users settings.", async (t) => { const res = await mockApi - .put("/api/v1/users/1/integrations/fervidex/cards") + .patch("/api/v1/users/1/integrations/fervidex/settings") .send({ cards: ["foo", "bar"], }) .set("Cookie", cookie); - t.strictSame(res.body.body, ["foo", "bar"]); + t.strictSame(res.body.body.cards, ["foo", "bar"]); const dbRes = await db["fer-settings"].findOne({ userID: 1 }); @@ -102,17 +94,17 @@ t.test("PUT /api/v1/users/:userID/integrations/fervidex/cards", async (t) => { t.end(); }); - t.test("Should insert a card filter document if one doesn't exist.", async (t) => { + t.test("Should insert a setting filter document if one doesn't exist.", async (t) => { await db["fer-settings"].remove({}); const res = await mockApi - .put("/api/v1/users/1/integrations/fervidex/cards") + .patch("/api/v1/users/1/integrations/fervidex/settings") .send({ cards: ["foo", "bar"], }) .set("Cookie", cookie); - t.strictSame(res.body.body, ["foo", "bar"]); + t.strictSame(res.body.body.cards, ["foo", "bar"]); const dbRes = await db["fer-settings"].findOne({ userID: 1 }); @@ -121,15 +113,15 @@ t.test("PUT /api/v1/users/:userID/integrations/fervidex/cards", async (t) => { t.end(); }); - t.test("Should null cards if null is provided.", async (t) => { + t.test("Should null settings if null is provided.", async (t) => { const res = await mockApi - .put("/api/v1/users/1/integrations/fervidex/cards") + .patch("/api/v1/users/1/integrations/fervidex/settings") .send({ cards: null, }) .set("Cookie", cookie); - t.strictSame(res.body.body, null); + t.strictSame(res.body.body.cards, null); const dbRes = await db["fer-settings"].findOne({ userID: 1 }); diff --git a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts index 38e4fe29a..b59f18209 100644 --- a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts +++ b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts @@ -1,10 +1,11 @@ import { Router } from "express"; -import p from "prudence"; import db from "external/mongo/db"; import { SYMBOL_TachiData } from "lib/constants/tachi"; import prValidate from "server/middleware/prudence-validate"; import { RequireKamaitachi } from "server/middleware/type-require"; import { RequireSelfRequestFromUser } from "../../middleware"; +import { optNull } from "utils/prudence"; +import { DeleteUndefinedProps } from "utils/misc"; const router: Router = Router({ mergeParams: true }); @@ -12,68 +13,74 @@ router.use(RequireKamaitachi); router.use(RequireSelfRequestFromUser); /** - * Retrieve all of your configured fervidex cards. Returns null instead - * of an empty array if no cards are configured. + * Retrieve your fervidex settings. * - * @name GET /api/v1/users/:userID/integrations/fervidex/cards + * @name GET /api/v1/users/:userID/integrations/fervidex/settings */ -router.get("/cards", async (req, res) => { +router.get("/settings", async (req, res) => { const user = req[SYMBOL_TachiData]!.requestedUser!; - const cardDoc = await db["fer-settings"].findOne({ + const settingsDoc = await db["fer-settings"].findOne({ userID: user.id, }); - if (!cardDoc || !cardDoc.cards) { - return res.status(200).json({ - success: true, - description: `No card filters enabled.`, - body: null, - }); - } - return res.status(200).json({ success: true, - description: `Found ${cardDoc.cards.length} card filters.`, - body: cardDoc.cards, + description: `Retrieved Fervidex settings.`, + body: settingsDoc ?? null, }); }); /** - * Replace your configured fervidex cards. Alternatively, pass null to disable - * the filter. + * Update your fervidex configuration. * - * @param cards - An array of strings or null. + * @param cards - An array of strings to be used as a cards whitelist. + * @param forceStaticImport - Whether or whether not to force a static import on non-INF2 clients. * - * @name PUT /api/v1/users/:userID/integrations/fervidex + * @name PUT /api/v1/users/:userID/integrations/fervidex/settings */ -router.put("/cards", prValidate({ cards: p.nullable(["string"]) }), async (req, res) => { - if (req.body.cards && req.body.cards.length > 6) { - return res.status(400).json({ - success: false, - description: `You cannot have more than 6 card filters at once.`, +router.patch( + "/settings", + prValidate({ cards: optNull(["string"]), forceStaticImport: "*?boolean" }), + async (req, res) => { + if (req.body.cards && req.body.cards.length > 6) { + return res.status(400).json({ + success: false, + description: `You cannot have more than 6 card filters at once.`, + }); + } + + const user = req[SYMBOL_TachiData]!.requestedUser!; + + const modifyDocument = req.body; + + DeleteUndefinedProps(modifyDocument); + + if (Object.keys(modifyDocument).length === 0) { + return res.status(400).json({ + success: false, + description: `No modifications sent.`, + }); + } + + await db["fer-settings"].update( + { userID: user.id }, + { + $set: modifyDocument, + }, + { + upsert: true, + } + ); + + const settings = await db["fer-settings"].findOne({ userID: user.id }); + + return res.status(200).json({ + success: true, + description: `Successfully updated settings.`, + body: settings, }); } - - const user = req[SYMBOL_TachiData]!.requestedUser!; - - await db["fer-settings"].update( - { userID: user.id }, - { - $set: { - cards: req.body.cards, - }, - }, - { - upsert: true, - } - ); - - return res.status(200).json({ - success: true, - description: `Successfully updated cards.`, - body: req.body.cards, - }); -}); +); export default router; From 61f792d8f96d4e1470f1fab14368bd50c80426c5 Mon Sep 17 00:00:00 2001 From: zkldi Date: Sat, 4 Sep 2021 04:00:59 +0100 Subject: [PATCH 6/6] Update fer profile/submit to honor forceStaticImport --- server/pnpm-lock.yaml | 8 +-- server/src/external/mongo/db.ts | 4 +- .../server/router/ir/fervidex/router.test.ts | 49 +++++++++++++++++++ .../src/server/router/ir/fervidex/router.ts | 11 ++++- 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index d98a6d95c..3358746e7 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -52,7 +52,7 @@ specifiers: rimraf: 3.0.2 safe-json-stringify: 1.2.0 supertest: 6.1.3 - tachi-common: 0.1.52 + tachi-common: 0.1.53 tap: 15.0.9 ts-node: 10.0.0 tsconfig-paths: 3.10.1 @@ -85,7 +85,7 @@ dependencies: redis: 3.1.2 rimraf: 3.0.2 safe-json-stringify: 1.2.0 - tachi-common: 0.1.52_ts-node@10.0.0+typescript@4.3.4 + tachi-common: 0.1.53_ts-node@10.0.0+typescript@4.3.4 typescript: 4.3.4 winston: 3.3.3 @@ -3666,8 +3666,8 @@ packages: strip-ansi: 6.0.0 dev: true - /tachi-common/0.1.52_ts-node@10.0.0+typescript@4.3.4: - resolution: {integrity: sha512-7QJw2r9Yb6DYWsLbDPFa4R73/LxVAsNXHsN2k4uDiI31EdYHzmQDRSbvwQcw6jEraOojRD9l5aLAemg/uYcm7g==} + /tachi-common/0.1.53_ts-node@10.0.0+typescript@4.3.4: + resolution: {integrity: sha512-KdfSo1xY11A/+Wj8cD5CqbExkYvG1p7J4PWNWmlZM57ZlF6yQ9tl6evcSPK9OaN2R391hqFXF5S7LDMQ1fHnrA==} dependencies: monk: 7.3.4 tap: 15.0.9_ts-node@10.0.0+typescript@4.3.4 diff --git a/server/src/external/mongo/db.ts b/server/src/external/mongo/db.ts index 445e5a0da..47a3eb42c 100644 --- a/server/src/external/mongo/db.ts +++ b/server/src/external/mongo/db.ts @@ -33,7 +33,7 @@ import { PublicUserDocument, OAuth2ApplicationDocument, integer, - FervidexCardsDocument, + FervidexSettingsDocument, } from "tachi-common"; import monk, { TMiddleware } from "monk"; import CreateLogCtx from "lib/logger/logger"; @@ -176,7 +176,7 @@ const db = { "oauth2-auth-codes": // i've inlined this one because i don't see it appearing anywhere else. monkDB.get<{ code: string; userID: integer; createdOn: number }>("oauth2-auth-codes"), - "fer-settings": monkDB.get("fer-settings"), + "fer-settings": monkDB.get("fer-settings"), }; export type StaticDatabases = diff --git a/server/src/server/router/ir/fervidex/router.test.ts b/server/src/server/router/ir/fervidex/router.test.ts index 801a8c0bf..669d18355 100644 --- a/server/src/server/router/ir/fervidex/router.test.ts +++ b/server/src/server/router/ir/fervidex/router.test.ts @@ -13,6 +13,7 @@ function TestHeaders(url: string, data: any) { await db["fer-settings"].insert({ userID: 1, cards: ["foo"], + forceStaticImport: false, }); const res = await mockApi @@ -368,5 +369,53 @@ t.test("POST /ir/fervidex/profile/submit", (t) => { t.end(); }); + t.test("Should allow requests from non INF2 if forceStaticImport is true.", async (t) => { + await db["fer-settings"].update({ userID: 1 }, { $set: { forceStaticImport: true } }); + await db.songs.iidx.remove({}); + await db.songs.iidx.insert(GetKTDataJSON("./tachi/tachi-songs-iidx.json")); + await db.charts.iidx.remove({}); + await db.charts.iidx.insert(GetKTDataJSON("./tachi/tachi-charts-iidx.json")); + + const res = await mockApi + .post("/ir/fervidex/profile/submit") + .set("Authorization", "Bearer mock_token") + .set("User-Agent", "fervidex/1.3.0") + .set("X-Software-Model", "LDJ:J:B:A:2020092900") + .send(ferStaticBody); + + t.equal(res.body.success, true, "Should be successful"); + + t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); + + t.strictSame( + res.body.body.classDeltas, + [ + { + set: "dan", + playtype: "SP", + old: null, + new: 15, + }, + ], + "Should return updated dan deltas." + ); + + const scores = await db.scores.count({ + service: "Fervidex Static", + }); + + t.equal(scores, 3, "Should import 3 scores."); + + const ugs = await db["game-stats"].findOne({ + userID: 1, + game: "iidx", + playtype: "SP", + }); + + t.equal(ugs!.classes.dan, 15, "Should successfully update dan to 9th."); + + t.end(); + }); + t.end(); }); diff --git a/server/src/server/router/ir/fervidex/router.ts b/server/src/server/router/ir/fervidex/router.ts index 2daa611b6..a3f41a9ea 100644 --- a/server/src/server/router/ir/fervidex/router.ts +++ b/server/src/server/router/ir/fervidex/router.ts @@ -69,7 +69,14 @@ const ValidateFervidexHeader: RequestHandler = (req, res, next) => { return next(); }; -const RequireInf2ModelHeader: RequestHandler = (req, res, next) => { +const RequireInf2ModelHeaderOrForceStatic: RequestHandler = async (req, res, next) => { + const settings = await db["fer-settings"].findOne({ userID: req[SYMBOL_TachiAPIAuth].userID! }); + + if (settings && settings.forceStaticImport) { + logger.debug(`User ${settings.userID} had forceStaticImport set, allowing request.`); + return next(); + } + const swModel = req.header("X-Software-Model"); if (!swModel) { @@ -193,7 +200,7 @@ router.use( * * @name POST /ir/fervidex/profile/submit */ -router.post("/profile/submit", RequireInf2ModelHeader, async (req, res) => { +router.post("/profile/submit", RequireInf2ModelHeaderOrForceStatic, async (req, res) => { const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!); const headers = {