mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-30 02:48:01 +03:00
Merge pull request #322 from zkldi:zkldi/issue-89-Fervidex-Card-Management
Support fervidex card filtering
This commit is contained in:
+1
-1
@@ -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.53",
|
||||
"typescript": "4.3.4",
|
||||
"winston": "3.3.3"
|
||||
},
|
||||
|
||||
Generated
+4
-4
@@ -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.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.50_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.50_ts-node@10.0.0+typescript@4.3.4:
|
||||
resolution: {integrity: sha512-F84jmrubIuhUSUZx7sm8aDpm43vklEgXT2j3XMaQoLLuuKIz5BgehMgNNNkJYoeRPvyEj/2Q/Bw46ye+D0q6Dg==}
|
||||
/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
|
||||
|
||||
Vendored
+3
@@ -33,6 +33,7 @@ import {
|
||||
PublicUserDocument,
|
||||
OAuth2ApplicationDocument,
|
||||
integer,
|
||||
FervidexSettingsDocument,
|
||||
} 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-settings": monkDB.get<FervidexSettingsDocument>("fer-settings"),
|
||||
};
|
||||
|
||||
export type StaticDatabases =
|
||||
@@ -205,6 +207,7 @@ export type StaticDatabases =
|
||||
| "user-private-information"
|
||||
| "oauth2-clients"
|
||||
| "oauth2-auth-codes"
|
||||
| "fer-settings"
|
||||
| "user-settings";
|
||||
|
||||
export type Databases = StaticDatabases | `songs-${Game}` | `charts-${Game}`;
|
||||
|
||||
Vendored
+1
@@ -91,6 +91,7 @@ const staticIndexes: Partial<Record<Databases, Index[]>> = {
|
||||
],
|
||||
"user-settings": [index({ userID: 1 }, UNIQUE)],
|
||||
"user-private-information": [index({ userID: 1 }, UNIQUE)],
|
||||
"fer-settings": [index({ userID: 1 }, UNIQUE)],
|
||||
};
|
||||
|
||||
const indexes: Partial<Record<Databases, Index[]>> = staticIndexes;
|
||||
|
||||
@@ -87,8 +87,8 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
const clientID = Random20Hex();
|
||||
const clientSecret = Random20Hex();
|
||||
const clientID = `CI${Random20Hex()}`;
|
||||
const clientSecret = `CS${Random20Hex()}`;
|
||||
|
||||
const clientDoc = {
|
||||
clientID,
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
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/s", async (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
const cookie = await CreateFakeAuthCookie(mockApi);
|
||||
|
||||
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/settings")
|
||||
.set("Cookie", cookie);
|
||||
|
||||
t.equal(res.statusCode, 200);
|
||||
|
||||
t.equal(res.body.body, null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
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/settings")
|
||||
.set("Cookie", cookie);
|
||||
|
||||
t.equal(res.statusCode, 200);
|
||||
|
||||
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/settings");
|
||||
|
||||
t.equal(res.statusCode, 401);
|
||||
|
||||
const res2 = await mockApi
|
||||
.get("/api/v1/users/1/integrations/fervidex/settings")
|
||||
.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/settings")
|
||||
.set("Cookie", cookie);
|
||||
|
||||
t.equal(res3.statusCode, 403);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
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 settings.", async (t) => {
|
||||
const res = await mockApi
|
||||
.patch("/api/v1/users/1/integrations/fervidex/settings")
|
||||
.send({
|
||||
cards: ["foo", "bar"],
|
||||
})
|
||||
.set("Cookie", cookie);
|
||||
|
||||
t.strictSame(res.body.body.cards, ["foo", "bar"]);
|
||||
|
||||
const dbRes = await db["fer-settings"].findOne({ userID: 1 });
|
||||
|
||||
t.strictSame(dbRes?.cards, ["foo", "bar"]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should insert a setting filter document if one doesn't exist.", async (t) => {
|
||||
await db["fer-settings"].remove({});
|
||||
|
||||
const res = await mockApi
|
||||
.patch("/api/v1/users/1/integrations/fervidex/settings")
|
||||
.send({
|
||||
cards: ["foo", "bar"],
|
||||
})
|
||||
.set("Cookie", cookie);
|
||||
|
||||
t.strictSame(res.body.body.cards, ["foo", "bar"]);
|
||||
|
||||
const dbRes = await db["fer-settings"].findOne({ userID: 1 });
|
||||
|
||||
t.strictSame(dbRes?.cards, ["foo", "bar"]);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should null settings if null is provided.", async (t) => {
|
||||
const res = await mockApi
|
||||
.patch("/api/v1/users/1/integrations/fervidex/settings")
|
||||
.send({
|
||||
cards: null,
|
||||
})
|
||||
.set("Cookie", cookie);
|
||||
|
||||
t.strictSame(res.body.body.cards, null);
|
||||
|
||||
const dbRes = await db["fer-settings"].findOne({ userID: 1 });
|
||||
|
||||
t.strictSame(dbRes?.cards, null);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Router } from "express";
|
||||
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 });
|
||||
|
||||
router.use(RequireKamaitachi);
|
||||
router.use(RequireSelfRequestFromUser);
|
||||
|
||||
/**
|
||||
* Retrieve your fervidex settings.
|
||||
*
|
||||
* @name GET /api/v1/users/:userID/integrations/fervidex/settings
|
||||
*/
|
||||
router.get("/settings", async (req, res) => {
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
|
||||
const settingsDoc = await db["fer-settings"].findOne({
|
||||
userID: user.id,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Retrieved Fervidex settings.`,
|
||||
body: settingsDoc ?? null,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Update your fervidex configuration.
|
||||
*
|
||||
* @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/settings
|
||||
*/
|
||||
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,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -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;
|
||||
|
||||
@@ -8,6 +8,38 @@ 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-settings"].remove({});
|
||||
await db["fer-settings"].insert({
|
||||
userID: 1,
|
||||
cards: ["foo"],
|
||||
forceStaticImport: false,
|
||||
});
|
||||
|
||||
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)
|
||||
@@ -337,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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -68,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) {
|
||||
@@ -150,7 +158,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-settings"].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,
|
||||
@@ -160,7 +200,7 @@ router.use(RequirePermissions("submit_score"), ValidateFervidexHeader, ValidateM
|
||||
*
|
||||
* @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 = {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"userID": 1,
|
||||
"cards": null,
|
||||
"forceStaticImport": false
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user