From 6fb1fff6c23cba2584b6e9ca3752c1aaee6c0cef Mon Sep 17 00:00:00 2001 From: zkldi Date: Tue, 25 May 2021 19:45:20 +0100 Subject: [PATCH] Git got stuck --- .../src/server/router/api/auth/auth.test.ts | 132 ------- server/src/server/router/api/auth/auth.ts | 156 --------- server/src/server/router/api/auth/router.ts | 286 ---------------- .../server/router/api/import/router.test.ts | 323 ------------------ server/src/server/router/api/import/router.ts | 108 ------ server/src/server/router/api/router.ts | 12 - server/src/server/router/router.ts | 4 +- 7 files changed, 2 insertions(+), 1019 deletions(-) delete mode 100644 server/src/server/router/api/auth/auth.test.ts delete mode 100644 server/src/server/router/api/auth/auth.ts delete mode 100644 server/src/server/router/api/auth/router.ts delete mode 100644 server/src/server/router/api/import/router.test.ts delete mode 100644 server/src/server/router/api/import/router.ts delete mode 100644 server/src/server/router/api/router.ts diff --git a/server/src/server/router/api/auth/auth.test.ts b/server/src/server/router/api/auth/auth.test.ts deleted file mode 100644 index 89080136c..000000000 --- a/server/src/server/router/api/auth/auth.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { - AddNewUserAPIKey, - CreateAPIKey, - CreateInviteCode, - AddNewInvite, - ReinstateInvite, - ValidateCaptcha, -} from "./auth"; -import t from "tap"; -import db, { CloseMongoConnection } from "../../../../external/mongo/db"; -import { PrivateUserDocument } from "kamaitachi-common"; -import { prAssert } from "../../../../test-utils/asserts"; -import Prudence from "prudence"; -import ResetDBState from "../../../../test-utils/reset-db-state"; -import { MockFetch } from "../../../../test-utils/mock-fetch"; - -t.test("#CreateAPIKey", (t) => { - t.match( - CreateAPIKey(), - /[0-9a-f]{20}/u, - "Should return a 20 character long lowercase hex string." - ); - - t.end(); -}); - -t.test("#AddNewUserAPIKey", (t) => { - t.beforeEach(ResetDBState); - - t.test("Should insert a new API key into the database", async (t) => { - const data = await AddNewUserAPIKey({ id: 1 } as PrivateUserDocument); - - t.not(data, null, "Return is not null"); - - prAssert( - data, - { - _id: Prudence.any, // lazy - apiKey: Prudence.regex(/[0-9a-f]{20}/u), - assignedTo: Prudence.is(1), - expireTime: Prudence.is(3176708633264), - permissions: { - selfkey: Prudence.is(true), - admin: Prudence.is(false), - }, - }, - "Data should match a public API key object" - ); - - const inDatabase = await db["public-api-keys"].findOne({ - _id: data._id, - }); - - t.strictSame(data, inDatabase, "Data from database is identical to data returned"); - - t.end(); - }); - - t.end(); -}); - -t.test("#ReinstateInvite", (t) => { - t.beforeEach(ResetDBState); - - t.test("Should change the 'consumed' property of an invite to true.", async (t) => { - // mock insert - const inviteDoc = await db.invites.insert({ - code: "foobar", - consumed: true, - createdBy: 1, - createdOn: 1, - }); - - const response = await ReinstateInvite(inviteDoc); - - t.equal(response.nModified, 1, "Should modify one document"); - - const invite2 = await db.invites.findOne({ - code: inviteDoc.code, // lol - }); - - t.equal(invite2!.consumed, false, "Should no longer be consumed"); - - t.end(); - }); - - 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); - - t.test("Should create a new invite from a given user", async (t) => { - const userDoc = await db.users.findOne({ id: 1 }); - - const result = await AddNewInvite(userDoc!); - - t.equal(result.createdBy, userDoc!.id, "Invite should be created by the requesting user."); - t.equal(result.consumed, false, "Invite should not be consumed."); - - // was created +/- 6 seconds from now. This is perhaps too lenient, but we're only really testing its just around now ish. - t.ok(Math.abs(result.createdOn - Date.now()) <= 6000, "Invite was created roughly now."); - - t.match(result.code, /^[0-9a-f]{40}$/u, "Invite code should be a 40 character hex string."); - }); - - t.end(); -}); - -t.test("#ValidateCaptcha", async (t) => { - t.equal( - await ValidateCaptcha("200", "bar", MockFetch({ status: 200 })), - true, - "Validates captcha when status return is 200" - ); - - t.equal( - await ValidateCaptcha("400", "bar", MockFetch({ status: 400 })), - false, - "Invalidates captcha when status return is not 200" - ); - - t.end(); -}); - -t.teardown(CloseMongoConnection); diff --git a/server/src/server/router/api/auth/auth.ts b/server/src/server/router/api/auth/auth.ts deleted file mode 100644 index b07859827..000000000 --- a/server/src/server/router/api/auth/auth.ts +++ /dev/null @@ -1,156 +0,0 @@ -import crypto from "crypto"; -import bcrypt from "bcrypt"; -import { - InviteCodeDocument, - PrivateUserDocument, - PublicAPIKeyDocument, - PublicUserDocument, -} from "kamaitachi-common"; -import { InsertResult } from "monk"; -import { CAPTCHA_SECRET_KEY } from "../../../../example-secrets"; -import db from "../../../../external/mongo/db"; -import { GetNextCounterValue } from "../../../../utils/db"; -import CreateLogCtx from "../../../../lib/logger/logger"; -import { FormatUserDoc } from "../../../../utils/user"; -import nodeFetch from "../../../../utils/fetch"; - -const logger = CreateLogCtx(__filename); - -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 CreateAPIKey(): string { - return crypto.randomBytes(20).toString("hex"); -} - -/** - * Despite these functions doing ultimately the same thing, they're separate incase they ever need to, - * you know, not do that. - * @returns A string - */ -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. - * @param password The hash to compare against. - */ -export function PasswordCompare(plaintext: string, password: string) { - return bcrypt.compare(plaintext, password); -} - -export function AddNewUserAPIKey( - privateUserDoc: PrivateUserDocument -): Promise> { - const apikey = CreateAPIKey(); - - const publicApiKeyDoc: PublicAPIKeyDocument = { - apiKey: apikey, - assignedTo: privateUserDoc.id, - expireTime: 3176708633264, - permissions: { - selfkey: true, // not sure what this was for but i'll keep it deliberately - admin: false, - }, - }; - - return db["public-api-keys"].insert(publicApiKeyDoc); -} - -export function ReinstateInvite(inviteDoc: InviteCodeDocument) { - logger.info(`Reinstated Invite ${inviteDoc.code}`); - return db.invites.update( - { - _id: inviteDoc._id, - }, - { - $set: { - consumed: false, - }, - } - ); -} - -export async function AddNewInvite(user: PublicUserDocument) { - const code = CreateInviteCode(); - - const result = await db.invites.insert({ - code, - consumed: false, - createdBy: user.id, - createdOn: Date.now(), - }); - - logger.info(`User ${FormatUserDoc(user)} created an invite.`); - - if (!result) { - logger.error( - `Fatal error in creating ${FormatUserDoc( - user - )}'s invite code. Database refused key ${code}.` - ); - throw new Error( - `Fatal error in creating ${FormatUserDoc( - user - )}'s invite code. Database refused key ${code}.` - ); - } - - return result; -} - -export async function AddNewUser( - username: string, - password: string, - email: string -): Promise> { - const hashedPassword = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS); - - logger.verbose(`Hashed password for ${username}.`); - - const userID = await GetNextCounterValue("users"); - - const userDoc: PrivateUserDocument = { - id: userID, - username: username, - usernameLowercase: username.toLowerCase(), - password: hashedPassword, - about: "I'm a fairly nondescript person.", - email: email, - clan: null, - friends: [], - socialMedia: {}, - settings: { - invisible: false, - nsfwSplashes: false, - }, - customBanner: false, - customPfp: false, - lastSeen: Date.now(), // lol - permissions: { - admin: false, // lol (2) - }, - }; - - return db.users.insert(userDoc); -} - -export async function ValidateCaptcha( - recaptcha: string, - remoteAddr: string | undefined, - fetch = nodeFetch -) { - const r = await fetch( - `https://www.google.com/recaptcha/api/siteverify?secret=${CAPTCHA_SECRET_KEY}&response=${recaptcha}&remoteip=${remoteAddr}` - ); - - if (r.status !== 200) { - logger.verbose(`Failed GCaptcha response ${r.status}, ${r.body}`); - } - - return r.status === 200; -} diff --git a/server/src/server/router/api/auth/router.ts b/server/src/server/router/api/auth/router.ts deleted file mode 100644 index c20cdc178..000000000 --- a/server/src/server/router/api/auth/router.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { Router } from "express"; -import Prudence from "prudence"; -import { - AddNewUser, - AddNewUserAPIKey, - PasswordCompare, - ReinstateInvite, - ValidatePassword, - ValidateCaptcha, -} from "./auth"; -import { - FormatUserDoc, - GetUserCaseInsensitive, - PRIVATEINFO_GetUserCaseInsensitive, -} from "../../../../utils/user"; - -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"; - -const logger = CreateLogCtx(__filename); - -const router: Router = Router({ mergeParams: true }); - -const LAZY_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/u; - -/* istanbul ignore next */ -const BASE_DOMAIN = process.env.NODE_ENV === "production" ? ".kamaitachi.xyz" : "127.0.0.1"; -const SHOULD_COOKIES_SECURE = process.env.NODE_ENV === "production"; - -/** - * Logs in a user. - * @name POST /api/auth/login - */ -router.post( - "/login", - prValidate( - { - username: Prudence.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u), - password: ValidatePassword, - captcha: "string", - }, - { - username: "Invalid username.", - captcha: "Please fill out the captcha.", - } - ), - async (req, res) => { - if (req.session.ktchi?.userID) { - logger.info(`Dual log-in attempted from ${req.session.ktchi.userID}`); - return res.status(409).json({ - success: false, - description: `You are already logged in as someone.`, - }); - } - - logger.verbose(`Recieved login request with username ${req.body.username} (${req.ip})`); - - /* istanbul ignore next */ - if (process.env.NODE_ENV === "production") { - logger.verbose("Validating captcha..."); - const validCaptcha = await ValidateCaptcha( - req.body.recaptcha, - req.socket.remoteAddress - ); - - if (!validCaptcha) { - logger.verbose("Captcha failed."); - return res.status(400).json({ - success: false, - description: `Captcha failed.`, - }); - } - - logger.verbose("Captcha validated!"); - } else { - logger.verbose("Skipped captcha check because not in production."); - } - - const requestedUser = await PRIVATEINFO_GetUserCaseInsensitive(req.body.username); - - if (!requestedUser) { - logger.verbose(`Invalid username for login ${req.body.username}.`); - return res.status(400).json({ - success: false, - description: `This user does not exist.`, - }); - } - - const passwordMatch = await PasswordCompare(req.body.password, requestedUser.password); - - if (!passwordMatch) { - logger.verbose("Invalid password provided."); - return res.status(400).json({ - success: false, - description: `Invalid password.`, - }); - } - - // username and password match up, we're good to check onwards - - let apiKeyDoc = await db["public-api-keys"].findOne({ - assignedTo: requestedUser.id, - "permissions.selfkey": true, - }); - - if (!apiKeyDoc) { - logger.warn( - `User ${FormatUserDoc(requestedUser)} did not have an apikey. Creating a new one.` - ); - - const newApiKey = await AddNewUserAPIKey(requestedUser); - - if (!newApiKey) { - logger.error( - `Bailed on user login ${FormatUserDoc( - requestedUser - )}. Could not create new apikey.` - ); - - throw new Error("FATAL in /register - apikey was unable to be created?"); - } - - apiKeyDoc = newApiKey; - } - - req.session.ktchi = { - userID: requestedUser.id, - apiKey: apiKeyDoc.apiKey, - }; - - req.session.cookie.maxAge = 3.154e10; // 1 year - - // API wants a cookie called "apikey" in order to make authorised requests. This might change. - res.cookie("apikey", apiKeyDoc.apiKey, { - maxAge: 3.154e10, - domain: BASE_DOMAIN, - secure: SHOULD_COOKIES_SECURE, - }); - - logger.verbose(`${FormatUserDoc(requestedUser)} Logged in.`); - - return res.status(200).json({ - success: true, - description: `Successfully logged in as ${FormatUserDoc(requestedUser)}`, - body: { - userID: requestedUser.id, - apiKey: apiKeyDoc.apiKey, - }, - }); - } -); - -/** - * Registers a new user. - * @name POST /api/auth/register - */ -router.post( - "/register", - prValidate( - { - username: Prudence.regex(/^[a-zA-Z_-][a-zA-Z0-9_-]{2,20}$/u), - password: ValidatePassword, - email: Prudence.regex(LAZY_EMAIL_REGEX), - inviteCode: "string", - captcha: "string", - }, - { - username: - "Usernames must be between 3 and 20 characters long, and can only contain alphanumeric characters!", - email: "Invalid email.", - inviteCode: "Invalid invite code.", - captcha: "Please fill out the captcha.", - } - ), - async (req, res) => { - logger.verbose(`Recieved register request with username ${req.body.username} (${req.ip})`); - - if (process.env.NODE_ENV === "production") { - logger.verbose("Validating captcha..."); - const validCaptcha = await ValidateCaptcha( - req.body.recaptcha, - req.socket.remoteAddress - ); - - if (!validCaptcha) { - logger.verbose("Captcha failed."); - return res.status(400).json({ - success: false, - description: `Captcha failed.`, - }); - } - - logger.verbose("Captcha validated!"); - } else { - logger.info("Skipped captcha check because not in production."); - } - - const existingUser = await GetUserCaseInsensitive(req.body.username); - - if (existingUser) { - logger.verbose(`Invalid username ${req.body.username}, already in use.`); - return res.status(409).json({ - success: false, - description: "This username is already in use.", - }); - } - - const inviteCodeDoc = await db.invites.findOneAndUpdate( - { - code: req.body.inviteCode, - consumed: false, - }, - { - $set: { - consumed: true, - }, - } - ); - - if (!inviteCodeDoc) { - logger.info(`Invalid invite code given: ${req.body.inviteCode}.`); - return res.status(401).json({ - success: false, - description: `This invite code is not valid.`, - }); - } - - logger.info(`Consumed invite ${inviteCodeDoc.code}.`); - - // if we get to this point, We're good to create the user. - - try { - const newUser = await AddNewUser(req.body.username, req.body.password, req.body.email); - - if (!newUser) { - throw new Error("AddNewUser failed to create a user."); - } - - const apiKeyDoc = await AddNewUserAPIKey(newUser); - - if (!apiKeyDoc) { - throw new Error("AddNewUserAPIKey failed to create an api key."); - } - - return res.status(200).json({ - success: true, - description: `Successfully created account ${req.body.username}!`, - body: { - id: newUser.id, - username: newUser.username, - }, - }); - } catch (err) { - logger.error( - `Bailed on user creation ${req.body.username} with invite code ${req.body.inviteCode}.`, - { err } - ); - - await ReinstateInvite(inviteCodeDoc); - - return res.status(500).json({ - success: false, - description: "An internal server error has occured.", - }); - } - } -); - -/** - * Logs out the requesting user. - * @name POST /api/auth/logout - */ -router.post("/logout", RequireLoggedIn, (req, res) => { - req.session.destroy(() => 0); - res.clearCookie("apikey"); - - return res.status(200).json({ - success: true, - description: `Logged Out.`, - body: {}, - }); -}); - -export default router; diff --git a/server/src/server/router/api/import/router.test.ts b/server/src/server/router/api/import/router.test.ts deleted file mode 100644 index 07c7973a5..000000000 --- a/server/src/server/router/api/import/router.test.ts +++ /dev/null @@ -1,323 +0,0 @@ -import t from "tap"; -import mockApi from "../../../../test-utils/mock-api"; -import { - GetKTDataBuffer, - LoadKTBlackIIDXData, - TestingIIDXEamusementCSV26, - TestingIIDXEamusementCSV27, -} 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 ResetDBState from "../../../../test-utils/reset-db-state"; -import db from "../../../../external/mongo/db"; - -// reset DB handles the post-stuff - -t.test("POST /api/import/file", async (t) => { - const cookie = await CreateFakeAuthCookie(mockApi); - - t.beforeEach(ResetDBState); - - RequireNeutralAuthentication("/api/import/file", "POST"); - - t.test("file/eamusement-iidx-csv", (t) => { - t.beforeEach(LoadKTBlackIIDXData); - - t.test("Mini HV import", async (t) => { - const res = await mockApi - .post("/api/import/file") - .set("Cookie", cookie) - .attach( - "scoreData", - GetKTDataBuffer("./eamusement-iidx-csv/small-hv-file.csv"), - "my_csv.csv" - ) - .field("importType", "file/eamusement-iidx-csv") - .field("playtype", "SP"); - - t.equal(res.body.success, true, "Should be successful."); - - t.equal(res.body.body.errors.length, 0, "Mini HV Import Should have 0 failed scores."); - - t.equal(res.body.body.scoreIDs.length, 2, "Should have 2 successful scores."); - - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); - - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); - - t.end(); - }); - - t.test("Valid Rootage CSV import", async (t) => { - const res = await mockApi - .post("/api/import/file") - .set("Cookie", cookie) - .attach("scoreData", TestingIIDXEamusementCSV26, "my_csv.csv") - .field("importType", "file/eamusement-iidx-csv") - .field("playtype", "SP"); - - t.equal(res.body.success, true, "Should be successful."); - - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); - - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); - - t.end(); - }); - - t.test("Valid Heroic Verse CSV import", async (t) => { - const res = await mockApi - .post("/api/import/file") - .set("Cookie", cookie) - .attach("scoreData", TestingIIDXEamusementCSV27, "my_csv.csv") - .field("importType", "file/eamusement-iidx-csv") - .field("playtype", "SP"); - - t.equal(res.body.success, true, "Should be successful."); - - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); - - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); - - t.end(); - }); - - t.end(); - }); - - // thats right i literally just copied it - t.test("file/pli-iidx-csv", (t) => { - t.beforeEach(LoadKTBlackIIDXData); - - t.test("Mini HV import", async (t) => { - const res = await mockApi - .post("/api/import/file") - .set("Cookie", cookie) - .attach( - "scoreData", - GetKTDataBuffer("./eamusement-iidx-csv/small-hv-file.csv"), - "my_csv.csv" - ) - .field("importType", "file/pli-iidx-csv") - .field("playtype", "SP"); - - t.equal(res.body.success, true, "Should be successful."); - - t.equal(res.body.body.errors.length, 0, "Mini HV Import Should have 0 failed scores."); - - t.equal(res.body.body.scoreIDs.length, 2, "Should have 2 successful scores."); - - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); - - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); - - t.end(); - }); - - t.test("Valid Rootage CSV import", async (t) => { - const res = await mockApi - .post("/api/import/file") - .set("Cookie", cookie) - .attach("scoreData", TestingIIDXEamusementCSV26, "my_csv.csv") - .field("importType", "file/pli-iidx-csv") - .field("playtype", "SP"); - - t.equal(res.body.success, true, "Should be successful."); - - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); - - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); - - t.end(); - }); - - t.test("Valid Heroic Verse CSV import", async (t) => { - const res = await mockApi - .post("/api/import/file") - .set("Cookie", cookie) - .attach("scoreData", TestingIIDXEamusementCSV27, "my_csv.csv") - .field("importType", "file/pli-iidx-csv") - .field("playtype", "SP"); - - t.equal(res.body.success, true, "Should be successful."); - - t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores."); - - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); - - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); - - t.end(); - }); - - t.end(); - }); - - t.test("file/batch-manual", (t) => { - t.test("Empty import", async (t) => { - const res = await mockApi - .post("/api/import/file") - .set("Cookie", cookie) - .attach( - "scoreData", - GetKTDataBuffer("./batch-manual/empty-file.json"), - "empty-file.json" - ) - .field("importType", "file/batch-manual"); - - t.equal(res.body.success, true, "Should be successful."); - - t.equal(res.body.body.errors.length, 0, "Import Should have 0 failed scores."); - - t.equal(res.body.body.scoreIDs.length, 0, "Should have 0 successful scores."); - - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); - - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); - - t.end(); - }); - - t.test("Single import", async (t) => { - const res = await mockApi - .post("/api/import/file") - .set("Cookie", cookie) - .attach( - "scoreData", - GetKTDataBuffer("./batch-manual/small-file.json"), - "small-file.json" - ) - .field("importType", "file/batch-manual"); - - t.equal(res.body.success, true, "Should be successful."); - - t.equal(res.body.body.errors.length, 0, "Import Should have 0 failed scores."); - - t.equal(res.body.body.scoreIDs.length, 1, "Should have 1 successful score."); - - const scoreCount = await db.scores.find({ - scoreID: { $in: res.body.body.scoreIDs }, - }); - - t.equal( - scoreCount.length, - res.body.body.scoreIDs.length, - "All returned scoreIDs should be inserted to the DB." - ); - - t.end(); - }); - - t.end(); - }); - - t.test("file/mer-iidx", (t) => { - t.beforeEach(LoadKTBlackIIDXData); - - t.test("Example Import", async (t) => { - const res = await mockApi - .post("/api/import/file") - .set("Cookie", cookie) - .attach("scoreData", GetKTDataBuffer("./mer/base.json"), "base.json") - .field("importType", "file/mer-iidx"); - - t.equal(res.body.success, true, "Should be successful"); - - t.equal(res.body.body.errors.length, 0, "Import Should have 0 failed scores."); - - t.equal(res.body.body.scoreIDs.length, 3, "Should have 3 successful scores."); - - t.end(); - }); - - t.test("Example Import", async (t) => { - const res = await mockApi - .post("/api/import/file") - .set("Cookie", cookie) - .attach("scoreData", GetKTDataBuffer("./mer/large.json"), "base.json") - .field("importType", "file/mer-iidx"); - - t.equal(res.body.success, true, "Should be successful"); - - t.equal(res.body.body.errors.length, 0, "Import Should have 0 failed scores."); - - t.equal(res.body.body.scoreIDs.length, 627, "Should have 627 successful scores."); - - t.end(); - }); - - t.end(); - }); - - t.test("file/solid-state-squad", (t) => { - t.beforeEach(LoadKTBlackIIDXData); - - t.test("Large Import", async (t) => { - const res = await mockApi - .post("/api/import/file") - .set("Cookie", cookie) - .attach("scoreData", GetKTDataBuffer("./s3/large-example.xml"), "large.xml") - .field("importType", "file/solid-state-squad"); - - t.equal(res.body.success, true, "Should be successful"); - t.equal(res.body.body.scoreIDs.length, null, "Should parse N scores."); - - t.end(); - }); - - t.end(); - }); - - t.end(); -}); - -t.teardown(CloseAllConnections); diff --git a/server/src/server/router/api/import/router.ts b/server/src/server/router/api/import/router.ts deleted file mode 100644 index 2e615d45f..000000000 --- a/server/src/server/router/api/import/router.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Router } from "express"; -import { FileUploadImportTypes } from "kamaitachi-common"; -import { fileImportTypes } from "kamaitachi-common/js/config"; -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 ScoreImportFatalError from "../../../../lib/score-import/framework/score-importing/score-import-error"; - -import { ExpressWrappedScoreImportMain } from "../../../../lib/score-import/framework/express-wrapper"; -import { CreateMulterSingleUploadMiddleware } from "../../../middleware/multer-upload"; - -const logger = CreateLogCtx(__filename); - -const router: Router = Router({ mergeParams: true }); - -const ParseMultipartScoredata = CreateMulterSingleUploadMiddleware("scoreData", logger); - -/** - * Import scores from a file. Expects the post request to be multipart, and to provide a scoreData file. - * @name POST /api/import/file - */ -router.post( - "/file", - RequireLoggedIn, - ParseMultipartScoredata, - prValidate( - { - importType: Prudence.isIn(fileImportTypes), - }, - {}, - { allowExcessKeys: true } - ), - async (req, res) => { - if (!req.file) { - return res.status(400).json({ - success: false, - description: `No file provided.`, - }); - } - - const importType = req.body.importType as FileUploadImportTypes; - - const inputParser = (logger: KtLogger) => - ResolveFileUploadData(importType, req.file, req.body, logger); - - const userDoc = await GetUserWithIDGuaranteed(req.session.ktchi!.userID); - - // The here is deliberate - TS picks the IIDX-CSV generic values - // for this function call because it sees them first - // but that is ABSOLUTELY not what is actually occuring. - // We use this as an override because we know better. - // see: https://www.typescriptlang.org/play?ts=4.3.0-beta#code/GYVwdgxgLglg9mABAQQDwBUB8AKYc4BciAYvhpgJSIDeiAsAFCKID0LiAJnAKYDOivKCGDBGAX0aMYYKNwBOwAIYRuJMlhqNmzAEaK5RdOMkNQkWAkQAlbkLlgAynAC23UnGxVqW7W0QAHOVtuMA5EKAALGH5o8IjVIN4QABsoRDhgARdVaX8QNOlBbkUwjMQ5RVCXH2YYTOwAWUVIgDoKqudPRFREAAYWgFYvGu1mILskWj0DRAAiQTlpAHNZxAkmbXXRkfGQexpEaaIARgAmAGY14wZGZNt0vfdEAF5rWz3HbPdPAG4TZGwcEe+AoPyAA - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const responseData = await ExpressWrappedScoreImportMain( - userDoc, - true, - importType, - inputParser - ); - - return res.status(responseData.statusCode).json(responseData.body); - } -); - -import ParseEamusementIIDXCSV from "../../../../lib/score-import/import-types/file/eamusement-iidx-csv/parser"; -import ParseBatchManual from "../../../../lib/score-import/import-types/file/batch-manual/parser"; -import { ParseSolidStateXML } from "../../../../lib/score-import/import-types/file/solid-state-squad/parser"; -import { ParseMerIIDX } from "../../../../lib/score-import/import-types/file/mer-iidx/parser"; -import ParsePLIIIDXCSV from "../../../../lib/score-import/import-types/file/pli-iidx-csv/parser"; - -/** - * Resolves the data from a file upload into an iterable, - * The appropriate processing function to map that iterable over, - * and and any context the processing may need (such as playtype) - * - * This also performs validation on the type of file uploaded. - * @param importType - The type of import request this was. - * @param fileData - The data sent by the user. - * @param body - Other data passed by the user in the request body. - */ -export function ResolveFileUploadData( - importType: FileUploadImportTypes, - fileData: Express.Multer.File, - body: Record, - logger: KtLogger -) { - switch (importType) { - case "file/eamusement-iidx-csv": - return ParseEamusementIIDXCSV(fileData, body, logger); - case "file/pli-iidx-csv": - return ParsePLIIIDXCSV(fileData, body, logger); - case "file/batch-manual": - return ParseBatchManual(fileData, body, logger); - case "file/solid-state-squad": - return ParseSolidStateXML(fileData, body, logger); - case "file/mer-iidx": - return ParseMerIIDX(fileData, body, logger); - default: - logger.error( - `importType ${importType} made it into ResolveFileUploadData, but should have been rejected by Prudence.` - ); - throw new ScoreImportFatalError(400, `Invalid importType of ${importType}.`); - } -} - -export default router; diff --git a/server/src/server/router/api/router.ts b/server/src/server/router/api/router.ts deleted file mode 100644 index fede8397e..000000000 --- a/server/src/server/router/api/router.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Router } from "express"; -import authRouter from "./auth/router"; -import importRouter from "./import/router"; -import irRouter from "../ir/router"; - -const router: Router = Router({ mergeParams: true }); - -router.use("/auth", authRouter); -router.use("/import", importRouter); -router.use("/ir", irRouter); - -export default router; diff --git a/server/src/server/router/router.ts b/server/src/server/router/router.ts index 572fa8ccf..2f28f64d9 100644 --- a/server/src/server/router/router.ts +++ b/server/src/server/router/router.ts @@ -1,10 +1,10 @@ import { Router } from "express"; -import apiRouter from "./api/router"; +import apiRouterV1 from "./api/v1/router"; import irRouter from "./ir/router"; const router: Router = Router({ mergeParams: true }); -router.use("/api/v1", apiRouter); +router.use("/api/v1", apiRouterV1); router.use("/ir", irRouter); export default router;