From 1bf8441d07e032dd5a125abdbdbbfc6c559a2280 Mon Sep 17 00:00:00 2001 From: zkldi Date: Fri, 21 May 2021 11:42:46 +0100 Subject: [PATCH] Groundwork for USC IR --- server/@types/express/index.d.ts | 5 +- server/pnpm-lock.yaml | 6 +- server/src/api/auth/auth.ts | 8 +- server/src/api/import/import.ts | 2 +- server/src/api/ir/fervidex/fervidex.ts | 150 ++++++------ server/src/api/ir/ir.ts | 2 + server/src/api/ir/usc/common.ts | 122 ++++++++++ server/src/api/ir/usc/usc.ts | 230 ++++++++++++++++++ server/src/common/database-lookup/chart.ts | 9 + server/src/constants/ktchi.ts | 1 + server/src/db/db.ts | 2 + .../import-types/ir/usc/parser.ts | 0 server/src/server.ts | 2 +- server/src/types.ts | 10 + 14 files changed, 458 insertions(+), 91 deletions(-) create mode 100644 server/src/api/ir/usc/common.ts create mode 100644 server/src/api/ir/usc/usc.ts create mode 100644 server/src/constants/ktchi.ts create mode 100644 server/src/score-import/import-types/ir/usc/parser.ts diff --git a/server/@types/express/index.d.ts b/server/@types/express/index.d.ts index 6ec1c17c4..16120d416 100644 --- a/server/@types/express/index.d.ts +++ b/server/@types/express/index.d.ts @@ -1,5 +1,6 @@ import { Session, SessionData } from "express-session"; -import { KtchiSessionData } from "../../src/types"; +import { SYMBOL_KtchiData } from "../../src/constants/ktchi"; +import { KtchiRequestData, KtchiSessionData } from "../../src/types"; export {}; @@ -15,6 +16,8 @@ declare global { namespace Express { interface Request { session: Session & Partial; + + [SYMBOL_KtchiData]?: Partial; } } } diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index 283749508..70f87b614 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -62,7 +62,7 @@ dependencies: express-session: 1.17.1 fast-json-stable-hash: 1.0.2 fast-xml-parser: 3.19.0 - kamaitachi-common: github.com/zkldi/Kamaitachi-common/d93f07f61f44116f709365af61606566765e297e + kamaitachi-common: github.com/zkldi/Kamaitachi-common/4f1a7215bfc4fc7ab140f94ae1a8f10dd69129b6 mongodb: 3.6.6 monk: 7.3.4 multer: 1.4.2 @@ -4644,8 +4644,8 @@ packages: '@types/yoga-layout': 1.9.2 dev: true - github.com/zkldi/Kamaitachi-common/d93f07f61f44116f709365af61606566765e297e: - resolution: {tarball: https://codeload.github.com/zkldi/Kamaitachi-common/tar.gz/d93f07f61f44116f709365af61606566765e297e} + github.com/zkldi/Kamaitachi-common/4f1a7215bfc4fc7ab140f94ae1a8f10dd69129b6: + resolution: {tarball: https://codeload.github.com/zkldi/Kamaitachi-common/tar.gz/4f1a7215bfc4fc7ab140f94ae1a8f10dd69129b6} name: kamaitachi-common version: 0.1.0 dependencies: diff --git a/server/src/api/auth/auth.ts b/server/src/api/auth/auth.ts index 1b09d9e2e..91618a50d 100644 --- a/server/src/api/auth/auth.ts +++ b/server/src/api/auth/auth.ts @@ -25,12 +25,13 @@ const router: Router = Router({ mergeParams: true }); const LAZY_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/u; +/* istanbul ignore next */ const BASE_DOMAIN = process.env.NODE_ENV === "production" ? ".kamaitachi.xyz" : "127.0.0.1"; const SHOULD_COOKIES_SECURE = process.env.NODE_ENV === "production"; /** * Logs in a user. - * @name /api/auth/login + * @name POST /api/auth/login */ router.post( "/login", @@ -56,6 +57,7 @@ router.post( logger.verbose(`Recieved login request with username ${req.body.username} (${req.ip})`); + /* istanbul ignore next */ if (process.env.NODE_ENV === "production") { logger.verbose("Validating captcha..."); const validCaptcha = await ValidateCaptcha( @@ -152,7 +154,7 @@ router.post( /** * Registers a new user. - * @name /api/auth/register + * @name POST /api/auth/register */ router.post( "/register", @@ -268,7 +270,7 @@ router.post( /** * Logs out the requesting user. - * @name /api/auth/logout + * @name POST /api/auth/logout */ router.post("/logout", RequireLoggedIn, (req, res) => { req.session.destroy(() => 0); diff --git a/server/src/api/import/import.ts b/server/src/api/import/import.ts index 95891bbfb..3cab1703b 100644 --- a/server/src/api/import/import.ts +++ b/server/src/api/import/import.ts @@ -42,7 +42,7 @@ const parseMultipartScoredata = (req: Request, res: Response, next: NextFunction /** * Import scores from a file. Expects the post request to be multipart, and to provide a scoreData file. - * @name /api/import/file + * @name POST /api/import/file */ router.post( "/file", diff --git a/server/src/api/ir/fervidex/fervidex.ts b/server/src/api/ir/fervidex/fervidex.ts index 1fc887591..e09f2cae0 100644 --- a/server/src/api/ir/fervidex/fervidex.ts +++ b/server/src/api/ir/fervidex/fervidex.ts @@ -119,6 +119,10 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => { }); } + if (softID.model === MODEL_INFINITAS_2) { + return next(); // allow anything for inf2. + } + if (softID.ext !== EXT_HEROIC_VERSE) { logger.info( `Rejected invalid Software Model ${softID.ext} from user ${ @@ -141,6 +145,8 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => { return next(); }; +router.use(RequireLoggedIn, ValidateFervidexHeader, ValidateModelHeader); + /** * Submits all of a users data to Kamaitachi. This data is extremely minimal, * as only a users Lamp and Score are sent. As such, this is not the prefered @@ -149,29 +155,23 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => { * * @name /api/ir/fervidex/profile/submit */ -router.post( - "/profile/submit", - RequireLoggedIn, - ValidateFervidexHeader, - RequireInf2ModelHeader, - async (req, res) => { - const userDoc = await GetUserWithIDGuaranteed(req.session.ktchi!.userID); +router.post("/profile/submit", RequireInf2ModelHeader, async (req, res) => { + const userDoc = await GetUserWithIDGuaranteed(req.session.ktchi!.userID); - const headers = { - // guaranteed to exist because of RequireInf2ModelHeader - model: req.header("X-Software-Model")!, - }; + const headers = { + // guaranteed to exist because of RequireInf2ModelHeader + model: req.header("X-Software-Model")!, + }; - const responseData = await ExpressWrappedScoreImportMain( - userDoc, - false, - "ir/fervidex-static", - (logger) => ParseFervidexStatic(req.body, headers, logger) - ); + const responseData = await ExpressWrappedScoreImportMain( + userDoc, + false, + "ir/fervidex-static", + (logger) => ParseFervidexStatic(req.body, headers, logger) + ); - return res.status(responseData.statusCode).json(responseData.body); - } -); + return res.status(responseData.statusCode).json(responseData.body); +}); /** * Submits a single score to Kamaitachi. In contrast to profile/submit, this @@ -180,37 +180,31 @@ router.post( * * @name /api/ir/fervidex/score/submit */ -router.post( - "/score/submit", - RequireLoggedIn, - ValidateFervidexHeader, - ValidateModelHeader, - async (req, res) => { - const userDoc = await GetUserWithIDGuaranteed(req.session.ktchi!.userID); +router.post("/score/submit", ValidateModelHeader, async (req, res) => { + const userDoc = await GetUserWithIDGuaranteed(req.session.ktchi!.userID); - const model = req.header("X-Software-Model"); + const model = req.header("X-Software-Model"); - if (!model) { - return res.status(400).json({ - success: false, - description: "No X-Software-Model header provided?", - }); - } - - const headers = { - model, - }; - - const responseData = await ExpressWrappedScoreImportMain( - userDoc, - true, - "ir/fervidex", - (logger) => ParseFervidexSingle(req.body, headers, logger) - ); - - return res.status(responseData.statusCode).json(responseData.body); + if (!model) { + return res.status(400).json({ + success: false, + description: "No X-Software-Model header provided?", + }); } -); + + const headers = { + model, + }; + + const responseData = await ExpressWrappedScoreImportMain( + userDoc, + true, + "ir/fervidex", + (logger) => ParseFervidexSingle(req.body, headers, logger) + ); + + return res.status(responseData.statusCode).json(responseData.body); +}); /** * Submits the result of a class to Kamaitachi. This contains the dan played @@ -218,43 +212,35 @@ router.post( * * @name /api/ir/fervidex/class/submit */ -router.post( - "/class/submit", - RequireLoggedIn, - ValidateFervidexHeader, - ValidateModelHeader, - async (req, res) => { - if (!req.body.cleared) { - return res - .status(200) - .json({ success: true, description: "No Update Made.", body: {} }); - } +router.post("/class/submit", ValidateModelHeader, async (req, res) => { + if (!req.body.cleared) { + return res.status(200).json({ success: true, description: "No Update Made.", body: {} }); + } - const classVal = FERVIDEX_COURSE_LOOKUP[req.body.course_id]; + const classVal = FERVIDEX_COURSE_LOOKUP[req.body.course_id]; - if (!classVal) { - return res.status(400).json({ - success: false, - description: `Invalid courseID of ${req.body.course_id}.`, - }); - } - - // is 0 or 1. - const playtype: Playtypes["iidx"] = req.body.playstyle ? "SP" : "DP"; - - const r = await UpdateClassIfGreater( - req.session.ktchi!.userID, - "iidx", - playtype, - "dan", - classVal - ); - - return res.status(200).json({ - success: true, - description: r === false ? "Dan unchanged." : "Dan changed!", + if (!classVal) { + return res.status(400).json({ + success: false, + description: `Invalid courseID of ${req.body.course_id}.`, }); } -); + + // is 0 or 1. + const playtype: Playtypes["iidx"] = req.body.playstyle ? "SP" : "DP"; + + const r = await UpdateClassIfGreater( + req.session.ktchi!.userID, + "iidx", + playtype, + "dan", + classVal + ); + + return res.status(200).json({ + success: true, + description: r === false ? "Dan unchanged." : "Dan changed!", + }); +}); export default router; diff --git a/server/src/api/ir/ir.ts b/server/src/api/ir/ir.ts index 18cfae817..9b350949f 100644 --- a/server/src/api/ir/ir.ts +++ b/server/src/api/ir/ir.ts @@ -3,6 +3,7 @@ import barbatosIR from "./barbatos/barbatos"; import chunitachiIR from "./chunitachi/chunitachi"; import directManualIR from "./direct-manual/direct-manual"; import fervidexIR from "./fervidex/fervidex"; +import uscIR from "./usc/usc"; const router: Router = Router({ mergeParams: true }); @@ -10,5 +11,6 @@ router.use("/barbatos", barbatosIR); router.use("/chunitachi", chunitachiIR); router.use("/direct-manual", directManualIR); router.use("/fervidex", fervidexIR); +router.use("/usc", uscIR); export default router; diff --git a/server/src/api/ir/usc/common.ts b/server/src/api/ir/usc/common.ts new file mode 100644 index 000000000..f2e69e6eb --- /dev/null +++ b/server/src/api/ir/usc/common.ts @@ -0,0 +1,122 @@ +import { integer, PBScoreDocument, ScoreDocument } from "kamaitachi-common"; +import CreateLogCtx from "../../../common/logger"; +import db from "../../../db/db"; + +const logger = CreateLogCtx("ir/usc/common.ts"); + +export interface USCServerScore { + score: integer; + lamp: 0 | 1 | 2 | 3 | 4 | 5; + timestamp: integer; + crit: integer; + near: integer; + error: integer; + ranking: integer; + gaugeMod: "NORMAL" | "HARD"; + noteMod: "NORMAL" | "MIRROR" | "RANDOM" | "MIR-RAN"; + username: string; +} + +export interface USCClientScore { + score: integer; + gameflags: integer; + gauge: number; + timestamp: integer; + crit: integer; + near: integer; + error: integer; + options: { + gaugeType: integer; + gaugeOpt: integer; + mirror: boolean; + random: boolean; + autoFlags: integer; //??? + }; + windows: { + perfect: number; + good: number; + hold: number; + miss: number; + slam: number; + }; +} + +export interface USCClientChart { + chartHash: string; + artist: string; + title: string; + level: integer; + difficulty: 0 | 1 | 2 | 3; + effector: string; + illustrator: string; + bpm: string; +} + +export const KT_LAMP_TO_USC: Record< + PBScoreDocument<"usc:Single">["scoreData"]["lamp"], + USCServerScore["lamp"] +> = { + // we don't do NO PLAY, so its not handled. + FAILED: 1, + CLEAR: 2, + "EXCESSIVE CLEAR": 3, + "ULTIMATE CHAIN": 4, + "PERFECT ULTIMATE CHAIN": 5, +}; + +/** + * Converts a Kamaitachi Score to the ServerScoreDocument + * as specified in the USCIR spec. This function silently + * returns sentinel values in the case that certain + * fields are null. + */ +export async function KtchiScoreToServerScore( + ktchiScore: PBScoreDocument<"usc:Single"> +): Promise { + const userDoc = await db.users.findOne( + { + id: ktchiScore.userID, + }, + { + projection: { + username: 1, + }, + } + ); + + if (!userDoc) { + logger.severe( + `User ${ktchiScore.userID} from PB on chart ${ktchiScore.chartID} has no user document?` + ); + throw new Error( + `User ${ktchiScore.userID} from PB on chart ${ktchiScore.chartID} has no user document?` + ); + } + + const scorePB = (await db.scores.findOne({ + scoreID: ktchiScore.composedFrom.scorePB, + })) as ScoreDocument<"usc:Single"> | null; + + if (!scorePB) { + logger.severe( + `Score ${ktchiScore.composedFrom.scorePB} does not exist, but is referenced in ${ktchiScore.userID}'s PBDoc on ${ktchiScore.chartID}?` + ); + + throw new Error( + `User ${ktchiScore.userID} from PB on chart ${ktchiScore.chartID} has no user document?` + ); + } + + return { + score: ktchiScore.scoreData.score, + timestamp: Math.floor((ktchiScore.timeAchieved ?? 0) / 1000), + crit: ktchiScore.scoreData.hitData.critical ?? 0, + near: ktchiScore.scoreData.hitData.near ?? 0, + error: ktchiScore.scoreData.hitData.miss ?? 0, + ranking: ktchiScore.rankingData.rank, + lamp: KT_LAMP_TO_USC[ktchiScore.scoreData.lamp], + username: userDoc.username, + noteMod: scorePB.scoreMeta.noteMod ?? "NORMAL", + gaugeMod: scorePB.scoreMeta.gaugeMod ?? "NORMAL", + }; +} diff --git a/server/src/api/ir/usc/usc.ts b/server/src/api/ir/usc/usc.ts new file mode 100644 index 000000000..a1a98bab9 --- /dev/null +++ b/server/src/api/ir/usc/usc.ts @@ -0,0 +1,230 @@ +import { Router, RequestHandler } from "express"; +import { FindChartOnSHA256 } from "../../../common/database-lookup/chart"; +import { SYMBOL_KtchiData } from "../../../constants/ktchi"; +import db from "../../../db/db"; +import { ChartDocument, PBScoreDocument } from "kamaitachi-common"; +import { AssertStrAsPositiveNonZeroInt } from "../../../score-import/framework/common/string-asserts"; +import CreateLogCtx from "../../../common/logger"; +import { KtchiScoreToServerScore } from "./common"; + +const logger = CreateLogCtx("usc.ts"); + +const router: Router = Router({ mergeParams: true }); + +const STATUS_CODES = { + UNAUTH: 41, + CHART_REFUSE: 42, + FORBIDDEN: 43, + NOT_FOUND: 44, + SERVER_ERROR: 50, + SUCCESS: 20, + BAD_REQ: 40, +}; + +// This is an implementation of the USCIR spec as per https://uscir.readthedocs.io. +// This specification always returns 200 OK, regardless of whether the result was okay +// as the HTTP code is used to determine whether the server received the request properly, +// rather than the result of the request. + +const ValidateUSCRequest: RequestHandler = async (req, res, next) => { + const token = req.header("Authorization"); + + if (!token) { + return res.status(200).json({ + statusCode: STATUS_CODES.UNAUTH, + description: "No auth token provided.", + }); + } + + const splitToken = token.split(" "); + + if (splitToken.length !== 2 || splitToken[0] !== "Bearer") { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: "Invalid Authorization Header. Expected Bearer ", + }); + } + + const uscAuthDoc = await db["usc-auth-tokens"].findOne({ + token: splitToken[1], + }); + + if (!uscAuthDoc) { + return res.status(200).json({ + statusCode: STATUS_CODES.UNAUTH, + description: "Unauthorized.", + }); + } + + req[SYMBOL_KtchiData] = { uscAuthDoc }; +}; + +router.use(ValidateUSCRequest); + +/** + * Used to check your connection to the server, and receive some basic information. + * https://uscir.readthedocs.io/en/latest/endpoints/heartbeat.html + * @name GET /api/ir/usc + */ +router.get("/", (req, res) => + res.status(200).json({ + statusCode: STATUS_CODES.SUCCESS, + description: "IR Request Successful.", + body: { + serverTime: Math.floor(Date.now() / 1000), + serverName: "Kamaitachi BLACK", + irVersion: "0.3.0-a", + }, + }) +); + +const RetrieveChart: RequestHandler = async (req, res, next) => { + const chart = await FindChartOnSHA256("usc", req.params.chartHash); + + req[SYMBOL_KtchiData]!.uscChartDoc = (chart ?? undefined) as + | ChartDocument<"usc:Single"> + | undefined; + + return next(); +}; + +const MAX_N = 10; + +/** + * Used to check if the server will accept a score for a given chart in advance of submitting it. + * https://uscir.readthedocs.io/en/latest/endpoints/chart-charthash.html + * @name GET /api/ir/usc/charts/:chartHash + */ +router.get("/charts/:chartHash", RetrieveChart, (req, res) => { + const chart = req[SYMBOL_KtchiData]!.uscChartDoc; + + if (!chart) { + return res.status(200).json({ + statusCode: STATUS_CODES.CHART_REFUSE, + description: "This chart is not tracked, and will not be accepted.", + }); + } + + return res.status(200).json({ + statusCode: STATUS_CODES.SUCCESS, + description: "This chart is tracked by the IR.", + }); +}); + +/** + * Used to retrieve the current server record for the chart with the specified hash. + * https://uscir.readthedocs.io/en/latest/endpoints/record.html + * @name GET /api/ir/usc/charts/:chartHash/record + */ +router.get("/charts/:chartHash/record", RetrieveChart, async (req, res) => { + const chart = req[SYMBOL_KtchiData]!.uscChartDoc; + + // spec ambigious here + + if (!chart) { + return res.status(200).json({ + statusCode: STATUS_CODES.CHART_REFUSE, + description: "This IR is not currently tracking this chart.", + }); + } + + const serverRecord = (await db["score-pbs"].findOne({ + chartID: chart.chartID, + "rankingData.rank": 1, + })) as PBScoreDocument<"usc:Single"> | null; + + if (!serverRecord) { + // @todo See uscir-spec #1 + return res.status(200).json({ + success: STATUS_CODES.SERVER_ERROR, + description: "TODO", + }); + } + + const serverScore = await KtchiScoreToServerScore(serverRecord); + + return res.status(200).json({ + success: STATUS_CODES.SUCCESS, + description: "Retrieved score.", + body: serverScore, + }); +}); + +/** + * Used to retrieve some particular useful subset of the scores from the server. + * https://uscir.readthedocs.io/en/latest/endpoints/leaderboard.html + * @name GET /api/ir/usc/charts/:chartHash/leaderboard + */ +router.get("/charts/:chartHash/leaderboard", RetrieveChart, async (req, res) => { + const chart = req[SYMBOL_KtchiData]!.uscChartDoc!; + + if (!(typeof req.query.mode === "string" && ["best", "rivals"].includes(req.query.mode))) { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: `Invalid 'mode' param - expected 'best' or 'rivals'.`, + }); + } + + if (typeof req.query.n !== "string") { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: `Invalid 'n' param - expected a positive non-zero integer less than or equal to ${MAX_N}.`, + }); + } + + let n; + + try { + n = AssertStrAsPositiveNonZeroInt(req.query.n, "Invalid 'N' param."); + } catch (err) { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: `Invalid 'n' param - expected a positive non-zero integer less than or equal to ${MAX_N}.`, + }); + } + + if (n >= MAX_N) { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: `Invalid 'n' param - expected a positive non-zero integer less than or equal to ${MAX_N}.`, + }); + } + + const mode = req.query.mode as "best" | "rivals"; + + if (mode === "rivals") { + return res.status(200).json({ + statusCode: STATUS_CODES.BAD_REQ, + description: "This is currently unsupported.", + }); + } + + const bestScores = (await db["score-pbs"].find( + { + chartID: chart.chartID, + }, + { + sort: { + "scoreData.perecent": -1, + }, + limit: n, + } + )) as PBScoreDocument<"usc:Single">[]; + + const serverScores = await Promise.all(bestScores.map(KtchiScoreToServerScore)); + + return res.status(200).json({ + statusCode: STATUS_CODES.SUCCESS, + description: `Returned ${serverScores.length} scores.`, + body: serverScores, + }); +}); + +/** + * Sends a score to the server. + * https://uscir.readthedocs.io/en/latest/endpoints/score-submit.html + * @name POST /api/ir/usc/scores + */ +router.post("/scores", async (req, res) => {}); + +export default router; diff --git a/server/src/common/database-lookup/chart.ts b/server/src/common/database-lookup/chart.ts index a83fdc293..e7619575b 100644 --- a/server/src/common/database-lookup/chart.ts +++ b/server/src/common/database-lookup/chart.ts @@ -199,3 +199,12 @@ export function FindSDVXChartOnInGameIDVersion( versions: version, }); } + +export function FindChartOnSHA256(game: Game, hash: string) { + // @todo #112 Reject requests to this for things that + // dont have sha256 props. + + return db.charts[game].findOne({ + "data.hashSHA256": hash, + }); +} diff --git a/server/src/constants/ktchi.ts b/server/src/constants/ktchi.ts new file mode 100644 index 000000000..486ac03ee --- /dev/null +++ b/server/src/constants/ktchi.ts @@ -0,0 +1 @@ +export const SYMBOL_KtchiData = Symbol("ktchi-request-patch"); diff --git a/server/src/db/db.ts b/server/src/db/db.ts index a13a7ea84..13f766018 100644 --- a/server/src/db/db.ts +++ b/server/src/db/db.ts @@ -8,6 +8,7 @@ import { TierlistParent, InviteCodeDocument, TierlistDataDocument, + USCAuthDocument, MilestoneDocument, NotificationDocument, FolderChartLookup, @@ -118,6 +119,7 @@ const db = { "iidx-eam-scores": monkDB.get("iidx-eam-scores"), "game-stats": monkDB.get("game-stats"), "kai-auth-tokens": monkDB.get("kai-auth-tokens"), + "usc-auth-tokens": monkDB.get("usc-auth-tokens"), }; export default db; diff --git a/server/src/score-import/import-types/ir/usc/parser.ts b/server/src/score-import/import-types/ir/usc/parser.ts new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/server.ts b/server/src/server.ts index 058b5618f..9db656dcb 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -56,7 +56,7 @@ app.use("/api", apiRouter); /** * If any user gets to this point, we send them index.html and let react router do the routing (and 404ing) - * @name * + * @name ALL * */ app.get("*", (req, res) => res.status(200).send("todo")); app.all("*", (req, res) => diff --git a/server/src/types.ts b/server/src/types.ts index 9c35306de..c509f263c 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -5,7 +5,9 @@ import { integer, ScoreDocument, AnySongDocument, + USCAuthDocument, MongoDBDocument, + ChartDocument, Game, Playtypes, } from "kamaitachi-common"; @@ -185,3 +187,11 @@ export interface ImportTypeContextMap { * Clarity type for empty objects - such as in context. */ export type EmptyObject = Record; + +/** + * Data that may be monkey-patched onto req.ktchi. This holds things such as middleware results. + */ +export interface KtchiRequestData { + uscAuthDoc?: USCAuthDocument; + uscChartDoc?: ChartDocument<"usc:Single">; +}