de-utils a lot of stuff

This commit is contained in:
zkldi
2021-05-25 18:09:09 +01:00
parent 7f1829daae
commit ae5f8a7e08
12 changed files with 58 additions and 57 deletions
-132
View File
@@ -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);
-156
View File
@@ -1,156 +0,0 @@
import crypto from "crypto";
import bcrypt from "bcrypt";
import {
InviteCodeDocument,
PrivateUserDocument,
PublicAPIKeyDocument,
PublicUserDocument,
} from "kamaitachi-common";
import db from "../external/mongo/db";
import { GetNextCounterValue } from "./db";
import { InsertResult } from "monk";
import CreateLogCtx from "./logger";
import { FormatUserDoc } from "./user";
import nodeFetch from "./fetch";
import { CAPTCHA_SECRET_KEY } from "../secrets";
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;
}
+2 -38
View File
@@ -1,46 +1,10 @@
import { MilestoneDocument, integer, GoalImportInfo } from "kamaitachi-common";
/**
* Processes and updates a user's milestones from their Goal Import Info (i.e. what is returned
* about goals from imports)
*/
export function ProcessMilestoneFromGII(
milestone: MilestoneDocument,
gii: Map<string, GoalImportInfo["new"]>
) {
const goalIDs = GetGoalIDsFromMilestone(milestone);
let progress = 0;
for (const goalID of goalIDs) {
const userInfo = gii.get(goalID);
if (!userInfo) {
continue;
}
if (!userInfo.achieved) {
continue;
}
progress++;
}
const outOf = CalculateMilestoneOutOf(milestone, goalIDs);
// milestone achieved!
if (progress >= outOf) {
return { achieved: true, progress };
}
return { achieved: false, progress };
}
/**
* Retrieves the goalID documents in a single array from the
* nested structure of milestones.
*/
function GetGoalIDsFromMilestone(milestone: MilestoneDocument) {
export function GetGoalIDsFromMilestone(milestone: MilestoneDocument) {
// this sucks - maybe a nicer way to do this, because nested
// maps are just ugly
return milestone.milestoneData.map((e) => e.goals.map((e) => e.goalID)).flat(1);
@@ -50,7 +14,7 @@ function GetGoalIDsFromMilestone(milestone: MilestoneDocument) {
* Work out how many goals need to be achieved for this
* milestone to be considered completed.
*/
function CalculateMilestoneOutOf(milestone: MilestoneDocument, goalIDs: string[]): integer {
export function CalculateMilestoneOutOf(milestone: MilestoneDocument, goalIDs: string[]): integer {
if (milestone.criteria.type === "all") {
return goalIDs.length;
} else if (milestone.criteria.type === "abs") {
-39
View File
@@ -1,39 +0,0 @@
// @todo #118
import { RequestHandler } from "express";
import multer, { MulterError } from "multer";
import CreateLogCtx from "./logger";
const defaultLogger = CreateLogCtx(__filename);
export const DefaultMulterUpload = multer({ limits: { fileSize: 1024 * 1024 * 16 } }); // 16MB
export const CreateMulterSingleUploadMiddleware = (
fieldName: string,
logger = defaultLogger
): RequestHandler => {
const UploadMW = DefaultMulterUpload.single(fieldName);
return (req, res, next) => {
UploadMW(req, res, (err: unknown) => {
if (err instanceof MulterError) {
logger.info(`Multer Error.`, { err });
return res.status(400).json({
success: false,
description:
"File provided was too large, corrupt, or provided in the wrong field.",
});
} else if (err) {
logger.error(`Unknown file import error: ${err}`, { err });
return res.status(500).json({
success: false,
description: `An internal server error has occured.`,
});
}
return next();
});
};
};