mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2024-09-22 00:06:16 +08:00
Merge branch 'master' of https://github.com/Miodec/monkeytype
This commit is contained in:
commit
b0eaa580f6
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
77
backend/dao/ape-keys.ts
Normal 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;
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -355,6 +355,10 @@ class UsersDAO {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async setApeKeys(uid, apeKeys) {
|
||||
await db.collection("users").updateOne({ uid }, { $set: { apeKeys } });
|
||||
}
|
||||
}
|
||||
|
||||
export default UsersDAO;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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,
|
||||
|
|
564
backend/package-lock.json
generated
564
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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",
|
||||
|
|
10
backend/types/types.d.ts
vendored
10
backend/types/types.d.ts
vendored
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue