This commit is contained in:
Miodec 2022-03-01 18:44:19 +01:00
commit b0eaa580f6
14 changed files with 712 additions and 67 deletions

View file

@ -1,25 +1,80 @@
import _ from "lodash";
import { randomBytes } from "crypto";
import { hash } from "bcrypt";
import ApeKeysDAO from "../../dao/ape-keys";
import MonkeyError from "../../handlers/error";
import { MonkeyResponse } from "../../handlers/monkey-response";
import { base64UrlEncode } from "../../handlers/misc";
const APE_KEY_BYTES = 48;
const SALT_ROUNDS = parseInt(process.env.APE_KEY_SALT_ROUNDS, 10) || 5;
function cleanApeKey(apeKey: MonkeyTypes.ApeKey): Partial<MonkeyTypes.ApeKey> {
return _.omit(apeKey, "hash");
}
class ApeKeysController {
static async getApeKey(_req: MonkeyTypes.Request): Promise<MonkeyResponse> {
return new MonkeyResponse("ApeKey retrieved");
static async getApeKeys(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const apeKeys = await ApeKeysDAO.getApeKeys(uid);
const hashlessKeys = _.mapValues(apeKeys, cleanApeKey);
return new MonkeyResponse("ApeKeys retrieved", hashlessKeys);
}
static async generateApeKey(
_req: MonkeyTypes.Request
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
return new MonkeyResponse("ApeKey generated");
const { name, enabled } = req.body;
const { uid } = req.ctx.decodedToken;
const { maxKeysPerUser } = req.ctx.configuration.apeKeys;
const currentNumberOfApeKeys = await ApeKeysDAO.countApeKeysForUser(uid);
if (currentNumberOfApeKeys >= maxKeysPerUser) {
throw new MonkeyError(
500,
"Maximum number of ApeKeys have been generated"
);
}
const apiKey = randomBytes(APE_KEY_BYTES).toString("base64url");
const saltyHash = await hash(apiKey, SALT_ROUNDS);
const apeKey: MonkeyTypes.ApeKey = {
name,
enabled,
hash: saltyHash,
createdOn: Date.now(),
modifiedOn: Date.now(),
};
const apeKeyId = await ApeKeysDAO.addApeKey(uid, apeKey);
return new MonkeyResponse("ApeKey generated", {
apeKey: base64UrlEncode(`${apeKeyId}.${apiKey}`),
apeKeyId,
apeKeyDetails: cleanApeKey(apeKey),
});
}
static async updateApeKey(
_req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
static async updateApeKey(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { apeKeyId } = req.params;
const { name, enabled } = req.body;
const { uid } = req.ctx.decodedToken;
await ApeKeysDAO.updateApeKey(uid, apeKeyId, name, enabled);
return new MonkeyResponse("ApeKey updated");
}
static async deleteApeKey(
_req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
static async deleteApeKey(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { apeKeyId } = req.params;
const { uid } = req.ctx.decodedToken;
await ApeKeysDAO.deleteApeKey(uid, apeKeyId);
return new MonkeyResponse("ApeKey deleted");
}
}

View file

@ -1,3 +1,4 @@
import _ from "lodash";
import UsersDAO from "../../dao/user";
import BotDAO from "../../dao/bot";
import { isUsernameValid } from "../../handlers/validation";
@ -7,6 +8,10 @@ import Logger from "./../../handlers/logger.js";
import uaparser from "ua-parser-js";
import { MonkeyResponse } from "../../handlers/monkey-response";
function cleanUser(user) {
return _.omit(user, "apeKeys");
}
class UserController {
static async createNewUser(req, _res) {
const { name } = req.body;
@ -123,7 +128,7 @@ class UserController {
agent.device.type;
}
Logger.log("user_data_requested", logobj, uid);
return new MonkeyResponse("User data retrieved", userInfo);
return new MonkeyResponse("User data retrieved", cleanUser(userInfo));
}
static async linkDiscord(req, _res) {

View file

@ -33,7 +33,7 @@ router.get(
"/",
RateLimit.apeKeysGet,
authenticateRequest(),
asyncHandler(ApeKeysController.getApeKey)
asyncHandler(ApeKeysController.getApeKeys)
);
router.post(

77
backend/dao/ape-keys.ts Normal file
View file

@ -0,0 +1,77 @@
import _ from "lodash";
import UsersDAO from "./user";
import { ObjectId } from "mongodb";
import MonkeyError from "../handlers/error";
function checkIfKeyExists(
apeKeys: MonkeyTypes.User["apeKeys"],
keyId: string
): void {
if (!_.has(apeKeys, keyId)) {
throw new MonkeyError(400, "Could not find ApeKey");
}
}
class ApeKeysDAO {
static async getApeKeys(uid: string): Promise<MonkeyTypes.User["apeKeys"]> {
const user = (await UsersDAO.getUser(uid)) as MonkeyTypes.User;
const userApeKeys = user.apeKeys ?? {};
return userApeKeys;
}
static async countApeKeysForUser(uid: string): Promise<number> {
const user = (await UsersDAO.getUser(uid)) as MonkeyTypes.User;
return _.size(user.apeKeys);
}
static async addApeKey(
uid: string,
apeKey: MonkeyTypes.ApeKey
): Promise<string> {
const user = (await UsersDAO.getUser(uid)) as MonkeyTypes.User;
const apeKeyId = new ObjectId().toHexString();
const apeKeys = {
...user.apeKeys,
[apeKeyId]: apeKey,
};
await UsersDAO.setApeKeys(uid, apeKeys);
return apeKeyId;
}
static async updateApeKey(
uid: string,
keyId: string,
name?: string,
enabled?: boolean
): Promise<void> {
const user = (await UsersDAO.getUser(uid)) as MonkeyTypes.User;
checkIfKeyExists(user.apeKeys, keyId);
const apeKey = user.apeKeys[keyId];
const updatedApeKey = {
...apeKey,
modifiedOn: Date.now(),
name: name ?? apeKey.name,
enabled: _.isNil(enabled) ? apeKey.enabled : enabled,
};
user.apeKeys[keyId] = updatedApeKey;
await UsersDAO.setApeKeys(uid, user.apeKeys);
}
static async deleteApeKey(uid: string, keyId: string): Promise<void> {
const user = (await UsersDAO.getUser(uid)) as MonkeyTypes.User;
checkIfKeyExists(user.apeKeys, keyId);
const apeKeys = _.omit(user.apeKeys, keyId);
await UsersDAO.setApeKeys(uid, apeKeys);
}
}
export default ApeKeysDAO;

View file

@ -1,8 +1,11 @@
import { InsertManyResult, InsertOneResult } from "mongodb";
import db from "../init/db";
async function addCommand(command, commandArguments): Promise<InsertOneResult> {
return await db.collection("bot-commands").insertOne({
async function addCommand(
command,
commandArguments
): Promise<InsertOneResult<any>> {
return await db.collection<any>("bot-commands").insertOne({
command,
arguments: commandArguments,
executed: false,

View file

@ -3,15 +3,15 @@ import db from "../init/db";
import _ from "lodash";
class ConfigDAO {
static async saveConfig(uid, config): Promise<UpdateResult> {
const configChanges = _.mapKeys(config, (value, key) => `config.${key}`);
static async saveConfig(uid: string, config: object): Promise<UpdateResult> {
const configChanges = _.mapKeys(config, (_value, key) => `config.${key}`);
return await db
.collection("configs")
.collection<any>("configs")
.updateOne({ uid }, { $set: configChanges }, { upsert: true });
}
static async getConfig(uid): Promise<object> {
const config = await db.collection("configs").findOne({ uid });
static async getConfig(uid: string): Promise<any> {
const config = await db.collection<any>("configs").findOne({ uid });
// if (!config) throw new MonkeyError(404, "Config not found");
return config;
}

View file

@ -355,6 +355,10 @@ class UsersDAO {
return null;
}
}
static async setApeKeys(uid, apeKeys) {
await db.collection("users").updateOne({ uid }, { $set: { apeKeys } });
}
}
export default UsersDAO;

View file

@ -32,3 +32,11 @@ export function identity(value) {
.replace(/^\[object\s+([a-z]+)\]$/i, "$1")
.toLowerCase();
}
export function base64UrlEncode(string) {
return Buffer.from(string).toString("base64url");
}
export function base64UrlDecode(string) {
return Buffer.from(string, "base64url").toString();
}

View file

@ -1,7 +1,6 @@
import {
AuthMechanism,
Collection,
Document,
Db,
MongoClient,
MongoClientOptions,
@ -10,7 +9,7 @@ import {
class DatabaseClient {
static mongoClient: MongoClient = null;
static db: Db = null;
static collections: Record<string, Collection<Document>> = {};
static collections: Record<string, Collection<any>> = {};
static connected = false;
static async connect(): Promise<void> {
@ -64,13 +63,13 @@ class DatabaseClient {
}
}
static collection(collectionName: string): Collection<Document> {
static collection<T>(collectionName: string): Collection<T> {
if (!this.connected) {
return null;
}
if (!(collectionName in this.collections)) {
this.collections[collectionName] = this.db.collection(collectionName);
this.collections[collectionName] = this.db.collection<T>(collectionName);
}
return this.collections[collectionName];

View file

@ -34,7 +34,7 @@ async function errorHandlingMiddleware(
"Oops! Our monkeys dropped their bananas. Please try again later.";
}
if (process.env.MODE !== "dev" && monkeyResponse.status > 400) {
if (process.env.MODE !== "dev" && monkeyResponse.status >= 500) {
const { uid, errorId } = monkeyResponse.data;
try {
@ -43,7 +43,7 @@ async function errorHandlingMiddleware(
`${monkeyResponse.status} ${error.message} ${error.stack}`,
uid
);
await db.collection("errors").insertOne({
await db.collection<any>("errors").insertOne({
_id: errorId,
timestamp: Date.now(),
status: monkeyResponse.status,

File diff suppressed because it is too large Load diff

View file

@ -16,6 +16,7 @@
"npm": "8.1.2"
},
"dependencies": {
"bcrypt": "5.0.1",
"cors": "2.8.5",
"cron": "1.8.2",
"dotenv": "10.0.0",
@ -38,6 +39,7 @@
"uuid": "8.3.2"
},
"devDependencies": {
"@types/bcrypt": "5.0.0",
"@types/cors": "2.8.12",
"@types/cron": "1.7.3",
"@types/lodash": "4.14.178",

View file

@ -42,7 +42,6 @@ declare namespace MonkeyTypes {
interface User {
// TODO, Complete the typings for the user model
_id: string;
addedAt: number;
bananas: number;
completedTests: number;
@ -53,20 +52,19 @@ declare namespace MonkeyTypes {
lbPersonalBests: object;
name: string;
personalBests: object;
quoteRatings: Record<string, Record<string, number>>;
quoteRatings?: Record<string, Record<string, number>>;
startedTests: number;
tags: object[];
timeTyping: number;
uid: string;
quoteMod: boolean;
cannotReport: boolean;
quoteMod?: boolean;
cannotReport?: boolean;
apeKeys?: Record<string, ApeKey>;
}
interface ApeKey {
_id: string;
name: string;
hash: string;
uid: string;
createdOn: number;
modifiedOn: number;
enabled: boolean;

View file

@ -23,7 +23,7 @@ main();
async function refactor(): Promise<void> {
console.log("getting all users");
const usersCollection = db.collection("users");
const usersCollection = db.collection<any>("users");
const users = await usersCollection.find({}).toArray();
console.log(users.length);