Huge API Permissions update

This commit is contained in:
zkldi
2021-06-10 20:13:32 +01:00
parent ed468f1c3d
commit b06ba4fd47
37 changed files with 373 additions and 480 deletions
+5 -1
View File
@@ -1,6 +1,7 @@
import { Session, SessionData } from "express-session";
import { SYMBOL_TachiData } from "../../src/lib/constants/tachi";
import { SYMBOL_TachiAPIData, SYMBOL_TachiData } from "../../src/lib/constants/tachi";
import { TachiRequestData, TachiSessionData } from "../../src/utils/types";
import { APITokenDocument } from "tachi-common";
export {};
@@ -18,6 +19,9 @@ declare global {
session: Session & Partial<SessionData>;
[SYMBOL_TachiData]?: Partial<TachiRequestData>;
// even though this is technically *not* present on every request
// it's always assigned in the main router, so its functionally equivalent.
[SYMBOL_TachiAPIData]: APITokenDocument;
}
}
}
+3 -3
View File
@@ -80,7 +80,7 @@ dependencies:
rate-limit-redis: 2.1.0
redis: 3.1.2
rimraf: 3.0.2
tachi-common: github.com/zkldi/tachi-common/cd7dd96729fd8d70b55165c90fb2bc62521ee293
tachi-common: github.com/zkldi/tachi-common/137fb72499a2ee6a98791ffea3961d0c69bf24b7
typescript: 4.3.2
winston: 3.3.3
@@ -4445,8 +4445,8 @@ packages:
engines: {node: '>=6'}
dev: true
github.com/zkldi/tachi-common/cd7dd96729fd8d70b55165c90fb2bc62521ee293:
resolution: {tarball: https://codeload.github.com/zkldi/tachi-common/tar.gz/cd7dd96729fd8d70b55165c90fb2bc62521ee293}
github.com/zkldi/tachi-common/137fb72499a2ee6a98791ffea3961d0c69bf24b7:
resolution: {tarball: https://codeload.github.com/zkldi/tachi-common/tar.gz/137fb72499a2ee6a98791ffea3961d0c69bf24b7}
name: kamaitachi-common
version: 0.1.0
dependencies:
+1
View File
@@ -83,6 +83,7 @@ const staticIndexes: Partial<Record<ValidDatabases, Index[]>> = {
"bms-course-lookup": [index({ md5sums: 1 }, UNIQUE)],
"beatoraja-auth-tokens": [index({ token: 1 }, UNIQUE)],
"usc-auth-tokens": [index({ token: 1 }, UNIQUE)],
"api-tokens": [index({ token: 1 }, UNIQUE)],
};
const indexes: Partial<Record<ValidDatabases, Index[]>> = staticIndexes;
+2 -4
View File
@@ -7,8 +7,8 @@ import {
ImportDocument,
TierlistParent,
InviteCodeDocument,
APITokenDocument,
TierlistDataDocument,
GenericAuthDocument,
MilestoneDocument,
NotificationDocument,
FolderChartLookup,
@@ -129,10 +129,8 @@ const db = {
"iidx-eam-scores": monkDB.get<IIDXEamusementScoreDocument>("iidx-eam-scores"),
"game-stats": monkDB.get<UserGameStats>("game-stats"),
"kai-auth-tokens": monkDB.get<KaiAuthDocument>("kai-auth-tokens"),
"usc-auth-tokens": monkDB.get<GenericAuthDocument>("usc-auth-tokens"),
"beatoraja-auth-tokens": monkDB.get<GenericAuthDocument>("beatoraja-auth-tokens"),
"bms-course-lookup": monkDB.get<BMSCourseDocument>("bms-course-lookup"),
"api-tokens": monkDB.get<APITokenDocument>("api-tokens"),
"orphan-scores": monkDB.get<OrphanScoreDocument>("orphan-scores"),
};
+1
View File
@@ -1 +1,2 @@
export const SYMBOL_TachiData = Symbol("tachi-request-patch");
export const SYMBOL_TachiAPIData = Symbol("tachi-api-auth-info");
+152
View File
@@ -0,0 +1,152 @@
import { RequestHandler } from "express";
import db from "../../external/mongo/db";
import { SYMBOL_TachiAPIData } from "../../lib/constants/tachi";
import { SplitAuthorizationHeader } from "../../utils/misc";
import { APITokenDocument, APIPermissions } from "tachi-common";
import CreateLogCtx from "../../lib/logger/logger";
const logger = CreateLogCtx(__filename);
const GuestToken: APITokenDocument = {
token: null,
userID: null,
identifier: "Guest Token",
permissions: {},
};
export const AllPermissions: Record<APIPermissions, true> = {
"create:goal": true,
"manage:goal": true,
"submit:score": true,
};
/**
* Sets the permissions for this request, alongside the user that is making the request.
*
* If this request was made with a valid Session Token, then a "self-key" is
* set as the request token.
*
* If this request was made with a valid Authorization: Bearer <token>, then the
* corresponding key is set as the request token.
*
* If this request was made with no auth headers or session tokens, then a guest
* token is set as the request token, with no permissions.
*
* This is set on req[SYMBOL_TachiAPIData].
*/
export const SetRequestPermissions: RequestHandler = async (req, res, next) => {
if (req.session.tachi?.userID) {
req[SYMBOL_TachiAPIData] = {
userID: req.session.tachi.userID,
identifier: `Session-Key ${req.session.tachi.userID}`,
token: null,
permissions: AllPermissions,
};
}
const header = req.header("Authorization");
// if no auth was attempted, default to the guest token.
if (!header) {
req[SYMBOL_TachiAPIData] = GuestToken;
return next();
}
const { token, type } = SplitAuthorizationHeader(header);
if (type !== "Bearer") {
return res.status(400).json({
success: false,
description: "Invalid Authorization Type - Expected Bearer.",
});
}
if (!token) {
return res.status(401).json({
success: false,
description: "Invalid token.",
});
}
const apiTokenData = await db["api-tokens"].findOne({
token,
});
if (!apiTokenData) {
return res.status(401).json({
success: false,
description: "The provided API token does not correspond with any key in the database.",
});
}
req[SYMBOL_TachiAPIData] = {
userID: apiTokenData.userID,
token,
permissions: apiTokenData.permissions,
identifier: apiTokenData.identifier,
};
return next();
};
/**
* Returns a middleware that enforces the request has the necessary permissions.
* @param perms - Rest Parameter. The set of permissions necessary to use this endpoint.
* @returns A middleware function.
*/
export const RequirePermissions =
(...perms: APIPermissions[]): RequestHandler =>
(req, res, next) => {
if (!req[SYMBOL_TachiAPIData]) {
logger.error(`RequirePermissions middleware was hit without any TachiAPIData?`);
return res.status(500).json({
success: false,
description: "An internal error has occured.",
});
}
const missingPerms = [];
for (const perm of perms) {
if (!req[SYMBOL_TachiAPIData]!.permissions[perm]) {
missingPerms.push(perm);
}
}
if (missingPerms.length > 0) {
logger.info(
`IP ${req.ip} - userID ${
req[SYMBOL_TachiAPIData].userID
} had insufficient permissions for request ${req.method} ${
req.url
}. ${missingPerms.join(", ")}`
);
return res.status(401).json({
success: false,
description: `You are missing the following permissions necessary for this request: ${missingPerms.join(
", "
)}`,
});
}
return next();
};
export const RequireNotGuest: RequestHandler = (req, res, next) => {
if (!req[SYMBOL_TachiAPIData]) {
logger.error(`RequirePermissions middleware was hit without any TachiAPIData?`);
return res.status(500).json({
success: false,
description: "An internal error has occured.",
});
}
if (!req[SYMBOL_TachiAPIData].userID) {
logger.info(`Request to ${req.method} ${req.url} was attempted by guest.`);
return res.status(401).json({
success: false,
description: "This endpoint requires authentication.",
});
}
return next();
};
@@ -1,10 +1,10 @@
import { RequireLoggedIn } from "./require-logged-in";
import { RequireLoggedInSession } from "./require-logged-in";
import expMiddlewareMock from "express-request-mock";
import t from "tap";
t.test("#RequireLoggedIn", (t) => {
t.test("Should reject users that are not logged in.", async (t) => {
const { res } = await expMiddlewareMock(RequireLoggedIn, {
const { res } = await expMiddlewareMock(RequireLoggedInSession, {
session: {
tachi: null,
},
@@ -21,7 +21,7 @@ t.test("#RequireLoggedIn", (t) => {
});
t.test("Should allow users that are logged in.", async (t) => {
const { res } = await expMiddlewareMock(RequireLoggedIn, {
const { res } = await expMiddlewareMock(RequireLoggedInSession, {
session: {
tachi: {
userID: 1,
@@ -1,9 +1,9 @@
import { NextFunction, Request, Response } from "express";
import { RequestHandler } from "express";
import CreateLogCtx from "../../lib/logger/logger";
const logger = CreateLogCtx(__filename);
export function RequireLoggedIn(req: Request, res: Response, next: NextFunction) {
export const RequireLoggedInSession: RequestHandler = (req, res, next) => {
if (!req.session.tachi?.userID) {
logger.info(`Received unauthorised request from ${req.ip} from ${req.originalUrl}`);
@@ -14,4 +14,16 @@ export function RequireLoggedIn(req: Request, res: Response, next: NextFunction)
}
next();
}
};
export const RequireNotLoggedInSession: RequestHandler = (req, res, next) => {
if (req.session.tachi?.userID) {
logger.info(`Dual log-in attempted from ${req.session.tachi.userID}`);
return res.status(409).json({
success: false,
description: `You cannot perform this while logged in.`,
});
}
return next();
};
+2 -2
View File
@@ -13,7 +13,7 @@ export const RequireBokutachi: RequestHandler = (req, res, next) => {
return res.status(404).send({
success: false,
description: `The route ${req.route} is only available on Bokutachi.`,
description: `The route ${req.url} is only available on Bokutachi.`,
});
};
@@ -29,6 +29,6 @@ export const RequireKamaitachi: RequestHandler = (req, res, next) => {
return res.status(404).send({
success: false,
description: `The route ${req.route} is only available on Kamaitachi.`,
description: `The route ${req.url} is only available on Kamaitachi.`,
});
};
@@ -1,4 +1,4 @@
import { CreateInviteCode, AddNewInvite, ReinstateInvite, ValidateCaptcha } from "./auth";
import { AddNewInvite, ReinstateInvite, ValidateCaptcha } from "./auth";
import t from "tap";
import db from "../../../../../external/mongo/db";
import ResetDBState from "../../../../../test-utils/resets";
@@ -33,12 +33,6 @@ t.test("#ReinstateInvite", (t) => {
t.end();
});
t.test("#CreateInviteCode", (t) => {
t.match(CreateInviteCode(), /^[0-9a-f]{40}$/u, "Invite should be a 40 character hex string.");
t.end();
});
t.test("#AddNewInvite", (t) => {
t.beforeEach(ResetDBState);
+5 -15
View File
@@ -1,11 +1,6 @@
import crypto from "crypto";
import bcrypt from "bcrypt";
import {
InviteCodeDocument,
PrivateUserDocument,
PublicAPIKeyDocument,
PublicUserDocument,
} from "tachi-common";
import { InviteCodeDocument, PrivateUserDocument, PublicUserDocument } from "tachi-common";
import { InsertResult } from "monk";
import db from "../../../../../external/mongo/db";
import { GetNextCounterValue } from "../../../../../utils/db";
@@ -13,6 +8,7 @@ import CreateLogCtx from "../../../../../lib/logger/logger";
import { FormatUserDoc } from "../../../../../utils/user";
import nodeFetch from "../../../../../utils/fetch";
import { CAPTCHA_SECRET_KEY } from "../../../../../lib/setup/config";
import { Random20Hex } from "../../../../../utils/misc";
const logger = CreateLogCtx(__filename);
@@ -21,10 +17,6 @@ const BCRYPT_SALT_ROUNDS = 12;
export const ValidatePassword = (self: unknown) =>
(typeof self === "string" && self.length >= 8) || "Passwords must be 8 characters or more.";
export function CreateInviteCode(): string {
return crypto.randomBytes(20).toString("hex");
}
/**
* Compares a plaintext string of a users password to a hash.
* @param plaintext The provided user input.
@@ -49,7 +41,7 @@ export function ReinstateInvite(inviteDoc: InviteCodeDocument) {
}
export async function AddNewInvite(user: PublicUserDocument) {
const code = CreateInviteCode();
const code = Random20Hex();
const result = await db.invites.insert({
code,
@@ -104,9 +96,6 @@ export async function AddNewUser(
customBanner: false,
customPfp: false,
lastSeen: Date.now(), // lol
permissions: {
admin: false, // lol (2)
},
};
return db.users.insert(userDoc);
@@ -123,7 +112,8 @@ export async function ValidateCaptcha(
if (r.status !== 200) {
logger.verbose(`Failed GCaptcha response ${r.status}, ${r.body}`);
return false;
}
return r.status === 200;
return true;
}
@@ -1,5 +1,4 @@
import t from "tap";
import db from "../../../../../external/mongo/db";
import { CloseAllConnections } from "../../../../../test-utils/close-connections";
import mockApi from "../../../../../test-utils/mock-api";
import ResetDBState from "../../../../../test-utils/resets";
+2 -16
View File
@@ -16,7 +16,7 @@ import {
import db from "../../../../../external/mongo/db";
import CreateLogCtx from "../../../../../lib/logger/logger";
import prValidate from "../../../../middleware/prudence-validate";
import { RequireLoggedIn } from "../../../../middleware/require-logged-in";
import { RequireLoggedInSession } from "../../../../middleware/require-logged-in";
const logger = CreateLogCtx(__filename);
@@ -24,20 +24,6 @@ const router: Router = Router({ mergeParams: true });
const LAZY_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/u;
/**
* Utility for checking whether you are logged in or not.
* @name POST /api/v1/auth/status
*/
router.post("/status", RequireLoggedIn, (req, res) =>
res.status(200).json({
success: true,
description: "Logged In.",
body: {
userID: req.session.tachi!.userID,
},
})
);
/**
* Logs in a user.
* @name POST /api/v1/auth/login
@@ -240,7 +226,7 @@ router.post(
* Logs out the requesting user.
* @name POST /api/v1/auth/logout
*/
router.post("/logout", RequireLoggedIn, (req, res) => {
router.post("/logout", RequireLoggedInSession, (req, res) => {
req.session.destroy(() => 0);
return res.status(200).json({
@@ -8,7 +8,7 @@ import {
} from "../../../../../test-utils/test-data";
import { CloseAllConnections } from "../../../../../test-utils/close-connections";
import { RequireNeutralAuthentication } from "../../../../../test-utils/api-common";
import { CreateFakeAuthCookie } from "../../../../../test-utils/fake-session";
import { CreateFakeAuthCookie } from "../../../../../test-utils/fake-auth";
import ResetDBState from "../../../../../test-utils/resets";
import db from "../../../../../external/mongo/db";
import { SetIndexes } from "../../../../../../scripts/set-indexes";
@@ -5,7 +5,7 @@ import Prudence from "prudence";
import { GetUserWithIDGuaranteed } from "../../../../../utils/user";
import CreateLogCtx, { KtLogger } from "../../../../../lib/logger/logger";
import prValidate from "../../../../middleware/prudence-validate";
import { RequireLoggedIn } from "../../../../middleware/require-logged-in";
import { RequireLoggedInSession } from "../../../../middleware/require-logged-in";
import ScoreImportFatalError from "../../../../../lib/score-import/framework/score-importing/score-import-error";
import { SIXTEEN_MEGABTYES } from "../../../../../lib/constants/filesize";
import { ExpressWrappedScoreImportMain } from "../../../../../lib/score-import/framework/express-wrapper";
@@ -34,7 +34,7 @@ const ParseMultipartScoredata = CreateMulterSingleUploadMiddleware(
*/
router.post(
"/file",
RequireLoggedIn,
RequireLoggedInSession,
RequireKamaitachi, // This is only useful for Kamaitachi, so.
ParseMultipartScoredata,
prValidate(
@@ -1,5 +1,5 @@
import { FormatVersion } from "../../../../../lib/constants/version";
import { CreateFakeAuthCookie } from "../../../../../test-utils/fake-session";
import { CreateFakeAuthCookie } from "../../../../../test-utils/fake-auth";
import mockApi from "../../../../../test-utils/mock-api";
import t from "tap";
import { CloseAllConnections } from "../../../../../test-utils/close-connections";
@@ -1,4 +1,5 @@
import { Router } from "express";
import { SYMBOL_TachiAPIData } from "../../../../../lib/constants/tachi";
import { FormatVersion } from "../../../../../lib/constants/version";
const router: Router = Router({ mergeParams: true });
@@ -15,6 +16,10 @@ router.get("/", (req, res) => {
body: {
serverTime: Date.now(),
version: FormatVersion(),
// converts {foo: true, bar: false, baz: true} into [foo, baz]
permissions: Object.entries(req[SYMBOL_TachiAPIData].permissions)
.filter((e) => e[1])
.map((e) => e[0]),
echo,
},
});
@@ -32,6 +37,10 @@ router.post("/", (req, res) => {
body: {
serverTime: Date.now(),
version: FormatVersion(),
// converts {foo: true, bar: false, baz: true} into [foo, baz]
permissions: Object.entries(req[SYMBOL_TachiAPIData].permissions)
.filter((e) => e[1])
.map((e) => e[0]),
echo,
},
});
@@ -1,7 +1,6 @@
import t from "tap";
import { RequireNeutralAuthentication } from "../../../../test-utils/api-common";
import { CloseAllConnections } from "../../../../test-utils/close-connections";
import { CreateFakeAuthCookie } from "../../../../test-utils/fake-session";
import { CreateFakeAuthCookie } from "../../../../test-utils/fake-auth";
import ResetDBState from "../../../../test-utils/resets";
import mockApi from "../../../../test-utils/mock-api";
import { TestingBarbatosScore } from "../../../../test-utils/test-data";
@@ -12,9 +11,6 @@ t.test("POST /ir/barbatos/score/submit", async (t) => {
t.beforeEach(ResetDBState);
// @TODO NEEDS TO USE PROPER AUTHENTICATION!!!
RequireNeutralAuthentication("/ir/barbatos/score/submit", "POST");
t.test("Should import a valid score", async (t) => {
const res = await mockApi
.post("/ir/barbatos/score/submit")
@@ -1,17 +1,20 @@
import { Router } from "express";
import { GetUserWithIDGuaranteed } from "../../../../utils/user";
import { RequireLoggedIn } from "../../../middleware/require-logged-in";
import { ExpressWrappedScoreImportMain } from "../../../../lib/score-import/framework/express-wrapper";
import { ParseBarbatosSingle } from "../../../../lib/score-import/import-types/ir/barbatos/parser";
import { RequirePermissions } from "../../../middleware/auth";
import { SYMBOL_TachiAPIData } from "../../../../lib/constants/tachi";
const router: Router = Router({ mergeParams: true });
router.use(RequirePermissions("submit:score"));
/**
* Submits a single score document from Barbatos clients.
* @name POST /ir/barbatos/score/submit
*/
router.post("/score/submit", RequireLoggedIn, async (req, res) => {
const userDoc = await GetUserWithIDGuaranteed(req.session.tachi!.userID);
router.post("/score/submit", async (req, res) => {
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIData]!.userID!);
const responseData = await ExpressWrappedScoreImportMain(
userDoc,
@@ -1,15 +1,13 @@
import t from "tap";
import { ValidateAuthToken, ValidateIRClientVersion } from "./auth";
import { ValidateIRClientVersion } from "./auth";
import expMiddlewareMock from "express-request-mock";
import ResetDBState from "../../../../test-utils/resets";
import { SYMBOL_TachiData } from "../../../../lib/constants/tachi";
import { CloseAllConnections } from "../../../../test-utils/close-connections";
t.test("#ValidateIRClientVersion", (t) => {
t.test("Should reject clients that are not supported", async (t) => {
const { res } = await expMiddlewareMock(ValidateIRClientVersion, {
headers: {
"X-TachiIR-Version": "1.2.0",
"X-BokutachiIR-Version": "1.2.0",
},
});
@@ -17,7 +15,7 @@ t.test("#ValidateIRClientVersion", (t) => {
t.equal(res.statusCode, 400);
t.equal(json.success, false);
t.match(json.description, /Invalid TachiIR client version/u);
t.match(json.description, /Invalid BokutachiIR client version/u);
t.end();
});
@@ -29,7 +27,7 @@ t.test("#ValidateIRClientVersion", (t) => {
t.equal(res.statusCode, 400);
t.equal(json.success, false);
t.match(json.description, /Invalid TachiIR client version/u);
t.match(json.description, /Invalid BokutachiIR client version/u);
t.end();
});
@@ -37,7 +35,7 @@ t.test("#ValidateIRClientVersion", (t) => {
t.test("Should accept 2.0.0", async (t) => {
const { res } = await expMiddlewareMock(ValidateIRClientVersion, {
headers: {
"X-TachiIR-Version": "2.0.0",
"X-BokutachiIR-Version": "2.0.0",
},
});
@@ -49,72 +47,4 @@ t.test("#ValidateIRClientVersion", (t) => {
t.end();
});
t.test("#ValidateAuthToken", (t) => {
t.beforeEach(ResetDBState);
t.test("Should reject invalid auth types", async (t) => {
const { res } = await expMiddlewareMock(ValidateAuthToken, {
headers: {
authorization: "NOTBEARER token",
},
});
const json = res._getJSONData();
t.equal(res.statusCode, 400);
t.equal(json.success, false);
t.match(json.description, /Invalid Authorization Type/u);
t.end();
});
t.test("Should reject unknown auth tokens", async (t) => {
const { res } = await expMiddlewareMock(ValidateAuthToken, {
headers: {
authorization: "Bearer InvalidToken",
},
});
const json = res._getJSONData();
t.equal(res.statusCode, 401);
t.equal(json.success, false);
t.match(json.description, /Unauthorised/u);
t.end();
});
t.test("Should reject no auth tokens", async (t) => {
const { res } = await expMiddlewareMock(ValidateAuthToken, {});
const json = res._getJSONData();
t.equal(res.statusCode, 400);
t.equal(json.success, false);
t.match(json.description, /No Authorization provided/u);
t.end();
});
t.test("Should allow existing auth tokens", async (t) => {
const { res, req } = await expMiddlewareMock(ValidateAuthToken, {
headers: {
authorization: "Bearer token",
},
});
t.equal(res.statusCode, 200);
t.hasStrict(
req[SYMBOL_TachiData]?.beatorajaAuthDoc,
{ token: "token", userID: 1 },
"Should attach the authDoc to the request TachiData"
);
t.end();
});
t.end();
});
t.teardown(CloseAllConnections);
+2 -41
View File
@@ -1,51 +1,12 @@
import { GenericAuthDocument } from "tachi-common";
import { RequestHandler } from "express";
import db from "../../../../external/mongo/db";
import { SplitAuthorizationHeader } from "../../../../utils/misc";
import { AssignToReqTachiData } from "../../../../utils/req-tachi-data";
export const ValidateAuthToken: RequestHandler = async (req, res, next) => {
const header = req.header("Authorization");
if (!header) {
return res.status(400).json({
success: false,
description: `No Authorization provided.`,
});
}
const { type, token } = SplitAuthorizationHeader(header);
if (type !== "Bearer") {
return res.status(400).json({
success: false,
description: `Invalid Authorization Type.`,
});
}
const beatorajaAuthDoc = (await db["beatoraja-auth-tokens"].findOne({
token,
})) as GenericAuthDocument | null;
if (!beatorajaAuthDoc) {
return res.status(401).json({
success: false,
description: "Unauthorised.",
});
}
AssignToReqTachiData(req, { beatorajaAuthDoc });
return next();
};
export const ValidateIRClientVersion: RequestHandler = (req, res, next) => {
const header = req.header("X-TachiIR-Version");
const header = req.header("X-BokutachiIR-Version");
if (header !== "2.0.0") {
return res.status(400).json({
success: false,
description: "Invalid TachiIR client version.",
description: "Invalid BokutachiIR client version.",
});
}
@@ -35,7 +35,7 @@ t.test("GET /ir/beatoraja/charts/:chartSHA256/scores", (t) => {
const res = await mockApi
.get(`/ir/beatoraja/charts/${GAZER_SHA256}/scores`)
.set("X-TachiIR-Version", "2.0.0")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token");
t.equal(res.status, 200);
@@ -46,7 +46,7 @@ t.test("GET /ir/beatoraja/charts/:chartSHA256/scores", (t) => {
t.test("Should return 404 if chart doesnt exist", async (t) => {
const res = await mockApi
.get(`/ir/beatoraja/charts/INVALID/scores`)
.set("X-TachiIR-Version", "2.0.0")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token");
t.equal(res.status, 404);
@@ -1,7 +1,7 @@
import { ChartDocument, PBScoreDocument } from "tachi-common";
import { Router, RequestHandler } from "express";
import db from "../../../../../external/mongo/db";
import { SYMBOL_TachiData } from "../../../../../lib/constants/tachi";
import { SYMBOL_TachiAPIData, SYMBOL_TachiData } from "../../../../../lib/constants/tachi";
import { TachiPBScoreToBeatorajaFormat } from "./convert-scores";
import { AssignToReqTachiData } from "../../../../../utils/req-tachi-data";
@@ -41,7 +41,7 @@ router.get("/scores", async (req, res) => {
// @optimisable - This should be solved with a couple queries and a hashmap.
const beatorajaScores = await Promise.all(
scores.map((e) =>
TachiPBScoreToBeatorajaFormat(e, chart, req[SYMBOL_TachiData]!.beatorajaAuthDoc!.userID)
TachiPBScoreToBeatorajaFormat(e, chart, req[SYMBOL_TachiAPIData]!.userID!)
)
);
@@ -6,119 +6,26 @@ import ResetDBState from "../../../../test-utils/resets";
import { GetKTDataJSON } from "../../../../test-utils/test-data";
import deepmerge from "deepmerge";
t.test("POST /ir/beatoraja/login", (t) => {
t.beforeEach(ResetDBState);
t.test("Should correctly return a token for correct credentials", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/login")
.send({
username: "test_zkldi",
password: "password",
})
.set("X-TachiIR-Version", "2.0.0");
t.equal(res.status, 200);
t.match(res.body.body.token, /^[0-9a-f]{40}$/u, "Should return a 20 byte long token.");
t.end();
});
t.test("Should reject invalid passwords", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/login")
.send({
username: "test_zkldi",
password: "invalid",
})
.set("X-TachiIR-Version", "2.0.0");
t.equal(res.status, 401);
t.equal(res.body.success, false);
t.equal(res.body.body, undefined);
t.end();
});
t.test("Should reject X-TachiIR-Versions", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/login")
.send({
username: "test_zkldi",
password: "password",
})
.set("X-TachiIR-Version", "1.2.0");
t.equal(res.status, 400);
t.equal(res.body.success, false);
t.equal(res.body.body, undefined);
const res2 = await mockApi.post("/ir/beatoraja/login").send({
username: "test_zkldi",
password: "password",
});
t.equal(res2.status, 400);
t.equal(res2.body.success, false);
t.equal(res2.body.body, undefined);
t.end();
});
t.test("Should reject users it cannot find.", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/login")
.send({
username: "invalid",
password: "password",
})
.set("X-TachiIR-Version", "2.0.0");
t.equal(res.status, 404);
t.equal(res.body.success, false);
t.equal(res.body.body, undefined);
t.end();
});
t.test("Should reject invalid request bodies.", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/login")
.send({})
.set("X-TachiIR-Version", "2.0.0");
t.equal(res.status, 400);
t.equal(res.body.success, false);
t.equal(res.body.body, undefined);
const res2 = await mockApi
.post("/ir/beatoraja/login")
.send({
username: { $where: "for(;;){}" },
password: 123,
})
.set("X-TachiIR-Version", "2.0.0");
t.equal(res2.status, 400);
t.equal(res2.body.success, false);
t.equal(res2.body.body, undefined);
t.end();
});
t.end();
});
t.test("POST /ir/beatoraja/submit-score", (t) => {
t.beforeEach(ResetDBState);
t.beforeEach(() =>
db["api-tokens"].insert({
userID: 1,
identifier: "Mock API Beatoraja Token",
permissions: {
"submit:score": true,
},
token: "mock_token",
})
);
const scoreReq = GetKTDataJSON("./beatoraja/base.json");
t.test("Should import a valid score.", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-score")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(scoreReq);
t.equal(res.status, 200);
@@ -154,8 +61,8 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
t.test("Should return an error if invalid client.", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-score")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(deepmerge(scoreReq, { client: "INVALID" }));
t.equal(res.status, 400);
@@ -169,8 +76,8 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
t.test("Should return an error if invalid score.", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-score")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(deepmerge(scoreReq, { score: { exscore: -1 } }));
t.equal(res.status, 400);
@@ -184,8 +91,8 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
t.test("Should return an error if invalid chart.", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-score")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(deepmerge(scoreReq, { chart: { title: null } }));
t.equal(res.status, 400);
@@ -254,12 +161,22 @@ const courseScore = {
t.test("POST /ir/beatoraja/submit-course", (t) => {
t.beforeEach(ResetDBState);
t.beforeEach(() =>
db["api-tokens"].insert({
userID: 1,
identifier: "Mock API Beatoraja Token",
permissions: {
"submit:score": true,
},
token: "mock_token",
})
);
t.test("Should accept a valid clear", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-course")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(courseScore);
t.equal(res.status, 200);
@@ -276,8 +193,8 @@ t.test("POST /ir/beatoraja/submit-course", (t) => {
t.test("Should silently reject a fail", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-course")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(deepmerge(courseScore, { score: { clear: "Failed" } }));
t.equal(res.status, 200);
@@ -290,8 +207,8 @@ t.test("POST /ir/beatoraja/submit-course", (t) => {
t.test("Should reject scores with no charts", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-course")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(deepmerge(courseScore, { course: { charts: [] } }, { arrayMerge: (d, s) => s }));
t.equal(res.status, 400);
@@ -304,8 +221,8 @@ t.test("POST /ir/beatoraja/submit-course", (t) => {
t.test("Should reject scores with invalid chart documents", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-course")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(deepmerge(courseScore, { course: { charts: [1, 2, 3, 4] } }));
t.equal(res.status, 400);
@@ -318,8 +235,8 @@ t.test("POST /ir/beatoraja/submit-course", (t) => {
t.test("Should reject scores with too many chart documents", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-course")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(
deepmerge(courseScore, {
course: { charts: [{ md5: "a" }, { md5: "a" }, { md5: "a" }, { md5: "a" }] },
@@ -336,8 +253,8 @@ t.test("POST /ir/beatoraja/submit-course", (t) => {
t.test("Should reject scores not on LN mode", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-course")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(
deepmerge(courseScore, {
score: {
@@ -356,8 +273,8 @@ t.test("POST /ir/beatoraja/submit-course", (t) => {
t.test("Should reject too few constraints", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-course")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(
deepmerge(
courseScore,
@@ -380,8 +297,8 @@ t.test("POST /ir/beatoraja/submit-course", (t) => {
t.test("Should reject non-array constraints", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-course")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(
deepmerge(
courseScore,
@@ -404,8 +321,8 @@ t.test("POST /ir/beatoraja/submit-course", (t) => {
t.test("Should reject too many constraints", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-course")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(
deepmerge(
courseScore,
@@ -428,8 +345,8 @@ t.test("POST /ir/beatoraja/submit-course", (t) => {
t.test("Should reject invalid constraints", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-course")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(
deepmerge(
courseScore,
@@ -452,8 +369,8 @@ t.test("POST /ir/beatoraja/submit-course", (t) => {
t.test("Should reject invalid constraints for 3", async (t) => {
const res = await mockApi
.post("/ir/beatoraja/submit-course")
.set("X-TachiIR-Version", "2.0.0")
.set("Authorization", "Bearer token")
.set("X-BokutachiIR-Version", "2.0.0")
.set("Authorization", "Bearer mock_token")
.send(
deepmerge(
courseScore,
@@ -1,18 +1,12 @@
import { Router } from "express";
import db from "../../../../external/mongo/db";
import { SYMBOL_TachiData } from "../../../../lib/constants/tachi";
import { SYMBOL_TachiAPIData } from "../../../../lib/constants/tachi";
import CreateLogCtx, { KtLogger } from "../../../../lib/logger/logger";
import { ExpressWrappedScoreImportMain } from "../../../../lib/score-import/framework/express-wrapper";
import { ParseBeatorajaSingle } from "../../../../lib/score-import/import-types/ir/beatoraja/parser";
import { UpdateClassIfGreater } from "../../../../utils/class";
import { Random20Hex } from "../../../../utils/misc";
import {
GetUserWithIDGuaranteed,
PRIVATEINFO_GetUserCaseInsensitive,
} from "../../../../utils/user";
import prValidate from "../../../middleware/prudence-validate";
import { PasswordCompare } from "../../api/v1/auth/auth";
import { ValidateAuthToken, ValidateIRClientVersion } from "./auth";
import { GetUserWithIDGuaranteed } from "../../../../utils/user";
import { ValidateIRClientVersion } from "./auth";
import chartsRouter from "./charts/router";
const logger = CreateLogCtx(__filename);
@@ -21,63 +15,13 @@ const router: Router = Router({ mergeParams: true });
router.use(ValidateIRClientVersion);
/**
* Takes a username and password and returns a unique auth token for the user
* to make ir requests with.
* @name POST /ir/beatoraja/login
*/
router.post(
"/login",
prValidate({
username: "string",
password: "string",
}),
async (req, res) => {
const userDoc = await PRIVATEINFO_GetUserCaseInsensitive(req.body.username);
if (!userDoc) {
return res.status(404).json({
success: false,
description: `The user ${req.body.username} does not exist.`,
});
}
const validPassword = await PasswordCompare(req.body.password, userDoc.password);
if (!validPassword) {
return res.status(401).json({
success: false,
description: `Invalid password.`,
});
}
// User is who they claim to be.
const token = Random20Hex();
await db["beatoraja-auth-tokens"].insert({
userID: userDoc.id,
token,
});
return res.status(200).json({
success: true,
description: `Successfully created auth token.`,
body: {
token,
},
});
}
);
router.use(ValidateAuthToken);
/**
* Submits a beatoraja score to Kamaitachi. If the chart is unavailable,
* store it as a new chart alongside the new score.
* @name POST /ir/beatoraja/submit-score
*/
router.post("/submit-score", async (req, res) => {
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiData]!.beatorajaAuthDoc!.userID);
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIData]!.userID!);
const ParserFunction = (logger: KtLogger) => ParseBeatorajaSingle(req.body, logger);
@@ -220,7 +164,7 @@ router.post("/submit-course", async (req, res) => {
});
}
const userID = req[SYMBOL_TachiData]!.beatorajaAuthDoc!.userID;
const userID = req[SYMBOL_TachiAPIData]!.userID!;
const result = await UpdateClassIfGreater(
userID,
@@ -1,26 +1,22 @@
import t from "tap";
import db from "../../../../external/mongo/db";
import { RequireNeutralAuthentication } from "../../../../test-utils/api-common";
import { CloseAllConnections } from "../../../../test-utils/close-connections";
import { CreateFakeAuthCookie } from "../../../../test-utils/fake-session";
import { InsertFakeTokenWithAllPerms } from "../../../../test-utils/fake-auth";
import mockApi from "../../../../test-utils/mock-api";
import ResetDBState from "../../../../test-utils/resets";
import { GetKTDataJSON } from "../../../../test-utils/test-data";
import deepmerge from "deepmerge";
t.test("POST /ir/chunitachi/import", async (t) => {
t.test("POST /ir/chunitachi/import", (t) => {
t.beforeEach(ResetDBState);
const cookie = await CreateFakeAuthCookie(mockApi);
RequireNeutralAuthentication("/ir/chunitachi/import", "POST");
t.beforeEach(InsertFakeTokenWithAllPerms("mock_token"));
const chunitachiBody = GetKTDataJSON("./batch-manual/chunitachi.json");
t.test("Should work for CHUNITACHI requests", async (t) => {
const res = await mockApi
.post("/ir/chunitachi/import")
.set("Cookie", cookie)
.set("Authorization", `Bearer mock_token`)
.send(chunitachiBody);
t.equal(res.body.success, true, "Should be successful");
@@ -35,7 +31,10 @@ t.test("POST /ir/chunitachi/import", async (t) => {
});
t.test("Should reject invalid batch-manual", async (t) => {
const res = await mockApi.post("/ir/chunitachi/import").set("Cookie", cookie).send({});
const res = await mockApi
.post("/ir/chunitachi/import")
.set("Authorization", `Bearer mock_token`)
.send({});
t.equal(res.body.success, false, "Should not be successful");
@@ -45,7 +44,7 @@ t.test("POST /ir/chunitachi/import", async (t) => {
t.test("Should reject batch-manual requests if game is not chunithm", async (t) => {
const res = await mockApi
.post("/ir/chunitachi/import")
.set("Cookie", cookie)
.set("Authorization", `Bearer mock_token`)
.send(deepmerge(chunitachiBody, { head: { game: "iidx" } }));
t.equal(res.body.success, false, "Should not be successful");
@@ -56,7 +55,7 @@ t.test("POST /ir/chunitachi/import", async (t) => {
t.test("Should reject batch-manual requests if service is not Chunitachi", async (t) => {
const res = await mockApi
.post("/ir/chunitachi/import")
.set("Cookie", cookie)
.set("Authorization", `Bearer mock_token`)
.send(deepmerge(chunitachiBody, { head: { service: "foo bar" } }));
t.equal(res.body.success, false, "Should not be successful");
@@ -1,8 +1,9 @@
import { Router } from "express";
import { GetUserWithIDGuaranteed } from "../../../../utils/user";
import { RequireLoggedIn } from "../../../middleware/require-logged-in";
import { ExpressWrappedScoreImportMain } from "../../../../lib/score-import/framework/express-wrapper";
import ParseDirectManual from "../../../../lib/score-import/import-types/ir/direct-manual/parser";
import { RequirePermissions } from "../../../middleware/auth";
import { SYMBOL_TachiAPIData } from "../../../../lib/constants/tachi";
const router: Router = Router({ mergeParams: true });
@@ -10,8 +11,8 @@ const router: Router = Router({ mergeParams: true });
* Submits a single score document from Chunitachi clients.
* @name POST /ir/chunitachi/score/submit
*/
router.post("/import", RequireLoggedIn, async (req, res) => {
const userDoc = await GetUserWithIDGuaranteed(req.session.tachi!.userID);
router.post("/import", RequirePermissions("submit:score"), async (req, res) => {
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIData].userID!);
if (req.body?.head?.game !== "chunithm") {
return res.status(400).json({
@@ -2,7 +2,7 @@ import t from "tap";
import db from "../../../../external/mongo/db";
import { RequireNeutralAuthentication } from "../../../../test-utils/api-common";
import { CloseAllConnections } from "../../../../test-utils/close-connections";
import { CreateFakeAuthCookie } from "../../../../test-utils/fake-session";
import { CreateFakeAuthCookie } from "../../../../test-utils/fake-auth";
import mockApi from "../../../../test-utils/mock-api";
import ResetDBState from "../../../../test-utils/resets";
import { GetKTDataJSON } from "../../../../test-utils/test-data";
@@ -1,8 +1,10 @@
import { Router } from "express";
import { GetUserWithIDGuaranteed } from "../../../../utils/user";
import { RequireLoggedIn } from "../../../middleware/require-logged-in";
import { RequireLoggedInSession } from "../../../middleware/require-logged-in";
import { ExpressWrappedScoreImportMain } from "../../../../lib/score-import/framework/express-wrapper";
import ParseDirectManual from "../../../../lib/score-import/import-types/ir/direct-manual/parser";
import { SYMBOL_TachiAPIData } from "../../../../lib/constants/tachi";
import { RequirePermissions } from "../../../middleware/auth";
const router: Router = Router({ mergeParams: true });
@@ -10,19 +12,24 @@ const router: Router = Router({ mergeParams: true });
* Imports scores in ir/json:direct-manual form.
* @name POST /ir/direct-manual/import
*/
router.post("/import", RequireLoggedIn, async (req, res) => {
const userDoc = await GetUserWithIDGuaranteed(req.session.tachi!.userID);
router.post(
"/import",
RequirePermissions("submit:score"),
RequireLoggedInSession,
async (req, res) => {
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIData].userID!);
const intent = req.header("X-User-Intent");
const intent = req.header("X-User-Intent");
const responseData = await ExpressWrappedScoreImportMain(
userDoc,
!!intent,
"ir/direct-manual",
(logger) => ParseDirectManual(req.body, logger)
);
const responseData = await ExpressWrappedScoreImportMain(
userDoc,
!!intent,
"ir/direct-manual",
(logger) => ParseDirectManual(req.body, logger)
);
return res.status(responseData.statusCode).json(responseData.body);
});
return res.status(responseData.statusCode).json(responseData.body);
}
);
export default router;
@@ -1,7 +1,7 @@
import t from "tap";
import db from "../../../../external/mongo/db";
import { CloseAllConnections } from "../../../../test-utils/close-connections";
import { CreateFakeAuthCookie } from "../../../../test-utils/fake-session";
import { CreateFakeAuthCookie } from "../../../../test-utils/fake-auth";
import mockApi from "../../../../test-utils/mock-api";
import ResetDBState from "../../../../test-utils/resets";
import { GetKTDataJSON } from "../../../../test-utils/test-data";
+26 -19
View File
@@ -3,12 +3,14 @@ import { UpdateClassIfGreater } from "../../../../utils/class";
import { GetUserWithIDGuaranteed } from "../../../../utils/user";
import { ParseEA3SoftID } from "../../../../utils/ea3id";
import { EXT_HEROIC_VERSE, MODEL_INFINITAS_2, REV_2DXBMS } from "../../../../lib/constants/ea3id";
import { RequireLoggedIn } from "../../../middleware/require-logged-in";
import { RequireLoggedInSession } from "../../../middleware/require-logged-in";
import { ExpressWrappedScoreImportMain } from "../../../../lib/score-import/framework/express-wrapper";
import { ParseFervidexStatic } from "../../../../lib/score-import/import-types/ir/fervidex-static/parser";
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_TachiAPIData } from "../../../../lib/constants/tachi";
import { RequirePermissions } from "../../../middleware/auth";
const logger = CreateLogCtx(__filename);
@@ -19,7 +21,7 @@ const ValidateFervidexHeader: RequestHandler = (req, res, next) => {
if (!agent) {
logger.debug(
`Rejected fervidex client with no agent from user ${req.session.tachi!.userID}.`
`Rejected fervidex client with no agent from user ${req[SYMBOL_TachiAPIData].userID!}.`
);
return res.status(400).json({
success: false,
@@ -29,9 +31,9 @@ const ValidateFervidexHeader: RequestHandler = (req, res, next) => {
if (!agent.startsWith("fervidex/")) {
logger.info(
`Rejected fervidex client with invalid agent ${agent} from user ${
req.session.tachi!.userID
}.`
`Rejected fervidex client with invalid agent ${agent} from user ${req[
SYMBOL_TachiAPIData
].userID!}.`
);
return res.status(400).json({
success: false,
@@ -43,9 +45,9 @@ const ValidateFervidexHeader: RequestHandler = (req, res, next) => {
if (!versions.every((e) => !Number.isNaN(e))) {
logger.info(
`Rejected fervidex client with agent ${agent} for NaN-like versions from user ${
req.session.tachi!.userID
}.`
`Rejected fervidex client with agent ${agent} for NaN-like versions from user ${req[
SYMBOL_TachiAPIData
].userID!}.`
);
return res.status(400).json({
success: false,
@@ -55,7 +57,9 @@ const ValidateFervidexHeader: RequestHandler = (req, res, next) => {
// version.minor
if (versions[1] < 3) {
logger.debug(`Rejected outdated fervidex client from user ${req.session.tachi!.userID}.`);
logger.debug(
`Rejected outdated fervidex client from user ${req[SYMBOL_TachiAPIData].userID!}.`
);
return res.status(400).json({
success: false,
description: `Versions of fervidex < 1.3.0 are not supported.`,
@@ -69,7 +73,9 @@ const RequireInf2ModelHeader: RequestHandler = (req, res, next) => {
const swModel = req.header("X-Software-Model");
if (!swModel) {
logger.debug(`Rejected empty X-Software-Model from user ${req.session.tachi!.userID}.`);
logger.debug(
`Rejected empty X-Software-Model from user ${req[SYMBOL_TachiAPIData].userID!}.`
);
return res.status(400).json({
success: false,
description: `Invalid X-Software-Model.`,
@@ -80,7 +86,7 @@ const RequireInf2ModelHeader: RequestHandler = (req, res, next) => {
const softID = ParseEA3SoftID(swModel);
if (softID.model !== MODEL_INFINITAS_2) {
logger.debug(`Rejected non-inf2 model from user ${req.session.tachi!.userID}.`);
logger.debug(`Rejected non-inf2 model from user ${req[SYMBOL_TachiAPIData].userID!}.`);
return res.status(400).send({
success: false,
description: "This endpoint is only available for INF2 clients.",
@@ -101,7 +107,9 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => {
const swModel = req.header("X-Software-Model");
if (!swModel) {
logger.debug(`Rejected empty X-Software Model from user ${req.session.tachi!.userID}.`);
logger.debug(
`Rejected empty X-Software Model from user ${req[SYMBOL_TachiAPIData].userID!}.`
);
return res.status(400).json({
success: false,
description: `Invalid X-Software-Model.`,
@@ -124,9 +132,8 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => {
if (softID.ext !== EXT_HEROIC_VERSE) {
logger.info(
`Rejected invalid Software Model ${softID.ext} from user ${
req.session.tachi!.userID
}.`
`Rejected invalid Software Model ${softID.ext} from user ${req[SYMBOL_TachiAPIData]
.userID!}.`
);
return res.status(400).json({
success: false,
@@ -144,7 +151,7 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => {
return next();
};
router.use(RequireLoggedIn, ValidateFervidexHeader, ValidateModelHeader);
router.use(RequirePermissions("submit:score"), ValidateFervidexHeader, ValidateModelHeader);
/**
* Submits all of a users data to Kamaitachi. This data is extremely minimal,
@@ -155,7 +162,7 @@ router.use(RequireLoggedIn, ValidateFervidexHeader, ValidateModelHeader);
* @name POST /ir/fervidex/profile/submit
*/
router.post("/profile/submit", RequireInf2ModelHeader, async (req, res) => {
const userDoc = await GetUserWithIDGuaranteed(req.session.tachi!.userID);
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIData].userID!);
const headers = {
// guaranteed to exist because of RequireInf2ModelHeader
@@ -180,7 +187,7 @@ router.post("/profile/submit", RequireInf2ModelHeader, async (req, res) => {
* @name POST /ir/fervidex/score/submit
*/
router.post("/score/submit", ValidateModelHeader, async (req, res) => {
const userDoc = await GetUserWithIDGuaranteed(req.session.tachi!.userID);
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIData].userID!);
const model = req.header("X-Software-Model");
@@ -243,7 +250,7 @@ router.post("/class/submit", ValidateModelHeader, async (req, res) => {
const playtype: Playtypes["iidx"] = req.body.play_style === 0 ? "SP" : "DP";
const r = await UpdateClassIfGreater(
req.session.tachi!.userID,
req[SYMBOL_TachiAPIData].userID!,
"iidx",
playtype,
"dan",
+8 -44
View File
@@ -1,6 +1,6 @@
import { Router, RequestHandler } from "express";
import { FindChartOnSHA256 } from "../../../../utils/queries/charts";
import { SYMBOL_TachiData } from "../../../../lib/constants/tachi";
import { SYMBOL_TachiAPIData, SYMBOL_TachiData } from "../../../../lib/constants/tachi";
import db from "../../../../external/mongo/db";
import {
ChartDocument,
@@ -19,7 +19,7 @@ import { CreateMulterSingleUploadMiddleware } from "../../../middleware/multer-u
import { AssignToReqTachiData } from "../../../../utils/req-tachi-data";
import { StoreCDN } from "../../../../lib/cdn/cdn";
import { ONE_MEGABYTE } from "../../../../lib/constants/filesize";
import crypto from "crypto";
import { RequirePermissions } from "../../../middleware/auth";
const logger = CreateLogCtx(__filename);
@@ -40,43 +40,6 @@ const STATUS_CODES = {
// 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.BAD_REQ,
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 <token>",
});
}
const uscAuthDoc = await db["usc-auth-tokens"].findOne({
token: splitToken[1],
});
if (!uscAuthDoc) {
return res.status(200).json({
statusCode: STATUS_CODES.UNAUTH,
description: "Unauthorized.",
});
}
AssignToReqTachiData(req, { uscAuthDoc });
return next();
};
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
@@ -88,7 +51,7 @@ router.get("/", (req, res) =>
description: "IR Request Successful.",
body: {
serverTime: Math.floor(Date.now() / 1000),
serverName: "Kamaitachi BLACK",
serverName: "Bokutachi",
irVersion: "0.3.1-a",
},
})
@@ -235,7 +198,7 @@ router.get("/charts/:chartHash/leaderboard", RetrieveChart, async (req, res) =>
* https://uscir.readthedocs.io/en/latest/endpoints/score-submit.html
* @name POST /ir/usc/scores
*/
router.post("/scores", async (req, res) => {
router.post("/scores", RequirePermissions("submit:score"), async (req, res) => {
if (typeof req.body.chart !== "object" || req.body.chart === null) {
return res.status(200).json({
statusCode: STATUS_CODES.BAD_REQ,
@@ -262,10 +225,10 @@ router.post("/scores", async (req, res) => {
});
}
const userDoc = await GetUserWithID(req[SYMBOL_TachiData]!.uscAuthDoc!.userID);
const userDoc = await GetUserWithID(req[SYMBOL_TachiAPIData]!.userID!);
if (!userDoc) {
logger.severe(`User ${req[SYMBOL_TachiData]!.uscAuthDoc!.userID} as no parent userDoc?`);
logger.severe(`User ${req[SYMBOL_TachiAPIData]!.userID!} as no parent userDoc?`);
return res.status(200).json({
statusCode: STATUS_CODES.SERVER_ERROR,
description: "An internal server error has occured.",
@@ -317,6 +280,7 @@ router.post("/scores", async (req, res) => {
*/
router.post(
"/replays",
RequirePermissions("submit:score"),
CreateMulterSingleUploadMiddleware("replay", ONE_MEGABYTE, logger),
async (req, res) => {
if (typeof req.body.identifier !== "string") {
@@ -334,7 +298,7 @@ router.post(
}
const correspondingScore = await db.scores.findOne({
userID: req[SYMBOL_TachiData]!.uscAuthDoc!.userID,
userID: req[SYMBOL_TachiAPIData]!.userID!,
game: "usc",
scoreID: req.body.identifier,
});
+3 -1
View File
@@ -1,11 +1,13 @@
import { Router } from "express";
import { SetRequestPermissions } from "../middleware/auth";
import { RateLimitMiddleware } from "../middleware/rate-limiter";
import apiRouterV1 from "./api/v1/router";
import irRouter from "./ir/router";
const router: Router = Router({ mergeParams: true });
router.use(RateLimitMiddleware);
// Add APIAuth and RateLimiting
router.use(SetRequestPermissions, RateLimitMiddleware);
router.use("/api/v1", apiRouterV1);
router.use("/ir", irRouter);
@@ -1,6 +1,8 @@
import ResetDBState from "./resets";
import CreateLogCtx from "../lib/logger/logger";
import supertest from "supertest";
import { AllPermissions } from "../server/middleware/auth";
import db from "../external/mongo/db";
const logger = CreateLogCtx(__filename);
@@ -21,3 +23,13 @@ export async function CreateFakeAuthCookie(mockApi: supertest.SuperTest<supertes
return res.headers["set-cookie"] as string[];
}
export function InsertFakeTokenWithAllPerms(token: string) {
return () =>
db["api-tokens"].insert({
userID: 1,
identifier: "Mock API Token",
permissions: AllPermissions,
token,
});
}
@@ -0,0 +1,10 @@
[
{
"userID": 1,
"token": "fake_api_token",
"permissions": {
"submit:goal": true
},
"identifier": "Mock Token For Testing"
}
]
@@ -1,4 +0,0 @@
[{
"userID": 1,
"token": "token"
}]
+1 -3
View File
@@ -1,4 +1,4 @@
import { integer, GenericAuthDocument, ChartDocument } from "tachi-common";
import { integer, ChartDocument } from "tachi-common";
declare module "express-session" {
// Inject additional properties on express-session
@@ -33,9 +33,7 @@ export type EmptyObject = Record<string, never>;
* Data that may be monkey-patched onto req.tachi. This holds things such as middleware results.
*/
export interface TachiRequestData {
uscAuthDoc?: GenericAuthDocument;
uscChartDoc?: ChartDocument<"usc:Single">;
beatorajaAuthDoc?: GenericAuthDocument;
beatorajaChartDoc?: ChartDocument<"bms:7K" | "bms:14K">;
}