mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-12 01:08:29 +03:00
Git got stuck
This commit is contained in:
@@ -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);
|
||||
@@ -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<InsertResult<PublicAPIKeyDocument>> {
|
||||
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<InsertResult<PrivateUserDocument>> {
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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 <any, any> 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<any, any>(
|
||||
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<string, unknown>,
|
||||
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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user