mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-04 12:58:11 +03:00
de-utils a lot of stuff
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 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;
|
||||
}
|
||||
@@ -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") {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user