mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-11 00:38:28 +03:00
Huge API Permissions update
This commit is contained in:
Vendored
+5
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
Vendored
+2
-4
@@ -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 +1,2 @@
|
||||
export const SYMBOL_TachiData = Symbol("tachi-request-patch");
|
||||
export const SYMBOL_TachiAPIData = Symbol("tachi-api-auth-info");
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,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">;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user