Migrate users controller (#2618)

* Migrate users controller + other things

* Undo import rename

* Fix return type

* Change status code

* Fix spacing
This commit is contained in:
Bruce Berrios 2022-03-03 14:50:06 -05:00 committed by GitHub
parent 2d4df4edf3
commit 0429c560ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 275 additions and 126 deletions

View file

@ -1,78 +1,75 @@
import _ from "lodash";
import UsersDAO from "../../dao/user";
import BotDAO from "../../dao/bot";
import { isUsernameValid } from "../../handlers/validation";
import MonkeyError from "../../handlers/error";
import fetch from "node-fetch";
import Logger from "./../../handlers/logger.js";
import uaparser from "ua-parser-js";
import Logger from "../../handlers/logger.js";
import { MonkeyResponse } from "../../handlers/monkey-response";
import { linkAccount } from "../../handlers/discord";
import { buildAgentLog } from "../../handlers/misc";
function cleanUser(user) {
function cleanUser(user: MonkeyTypes.User): Omit<MonkeyTypes.User, "apeKeys"> {
return _.omit(user, "apeKeys");
}
class UserController {
static async createNewUser(req, _res) {
static async createNewUser(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
const { name } = req.body;
const { email, uid } = req.ctx.decodedToken;
await UsersDAO.addUser(name, email, uid);
Logger.log("user_created", `${name} ${email}`, uid);
return new MonkeyResponse("User created");
}
static async deleteUser(req, _res) {
static async deleteUser(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const userInfo = await UsersDAO.getUser(uid);
const userInfo = await UsersDAO.getUser(uid);
await UsersDAO.deleteUser(uid);
Logger.log("user_deleted", `${userInfo.email} ${userInfo.name}`, uid);
return new MonkeyResponse("User deleted");
}
static async updateName(req, _res) {
static async updateName(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const { name } = req.body;
if (!isUsernameValid(name))
throw new MonkeyError(
400,
"Username invalid. Name cannot contain special characters or contain more than 14 characters. Can include _ . and -"
);
let olduser = await UsersDAO.getUser(uid);
const oldUser = await UsersDAO.getUser(uid);
await UsersDAO.updateName(uid, name);
Logger.log(
"user_name_updated",
`changed name from ${olduser.name} to ${name}`,
`changed name from ${oldUser.name} to ${name}`,
uid
);
return new MonkeyResponse("User's name updated");
}
static async clearPb(req, _res) {
static async clearPb(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
await UsersDAO.clearPb(uid);
Logger.log("user_cleared_pbs", "", uid);
return new MonkeyResponse("User's PB cleared");
}
static async checkName(req, _res) {
static async checkName(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { name } = req.params;
if (!isUsernameValid(name)) {
throw new MonkeyError(
400,
"Username invalid. Name cannot contain special characters or contain more than 14 characters. Can include _ . and -"
);
const available = await UsersDAO.isNameAvailable(name);
if (!available) {
throw new MonkeyError(409, "Username unavailable");
}
const available = await UsersDAO.isNameAvailable(name);
if (!available) throw new MonkeyError(400, "Username unavailable");
return new MonkeyResponse("Username available");
}
static async updateEmail(req, _res) {
static async updateEmail(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const { newEmail } = req.body;
@ -81,11 +78,13 @@ class UserController {
} catch (e) {
throw new MonkeyError(400, e.message, "update email", uid);
}
Logger.log("user_email_updated", `changed email to ${newEmail}`, uid);
return new MonkeyResponse("Email updated");
}
static async getUser(req, _res) {
static async getUser(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { email, uid } = req.ctx.decodedToken;
let userInfo;
@ -103,133 +102,109 @@ class UserController {
);
}
}
let agent = uaparser(req.headers["user-agent"]);
let logobj = {
ip:
req.headers["cf-connecting-ip"] ||
req.headers["x-forwarded-for"] ||
req.ip ||
"255.255.255.255",
agent:
agent.os.name +
" " +
agent.os.version +
" " +
agent.browser.name +
" " +
agent.browser.version,
};
if (agent.device.vendor) {
logobj.device =
agent.device.vendor +
" " +
agent.device.model +
" " +
agent.device.type;
}
Logger.log("user_data_requested", logobj, uid);
const agentLog = buildAgentLog(req);
Logger.log("user_data_requested", agentLog, uid);
return new MonkeyResponse("User data retrieved", cleanUser(userInfo));
}
static async linkDiscord(req, _res) {
static async linkDiscord(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const {
data: { tokenType, accessToken },
} = req.body;
let requser;
try {
requser = await UsersDAO.getUser(uid);
} catch (e) {
requser = null;
}
if (requser?.banned === true) {
const userInfo = await UsersDAO.getUser(uid);
if (userInfo.banned) {
throw new MonkeyError(403, "Banned accounts cannot link with Discord");
}
const discordFetch = await fetch("https://discord.com/api/users/@me", {
headers: {
authorization: `${req.body.data.tokenType} ${req.body.data.accessToken}`,
},
});
const discordFetchJSON = await discordFetch.json();
const did = discordFetchJSON.id;
if (!did) {
const { id: discordId } = await linkAccount(tokenType, accessToken);
if (!discordId) {
throw new MonkeyError(
500,
"Could not get Discord account info",
"did is undefined"
"discord id is undefined"
);
}
let user;
try {
user = await UsersDAO.getUserByDiscordId(did);
} catch (e) {
user = null;
}
if (user !== null) {
const discordIdAvailable = await UsersDAO.isDiscordIdAvailable(discordId);
if (!discordIdAvailable) {
throw new MonkeyError(
400,
"This Discord account is already linked to a different account"
);
}
await UsersDAO.linkDiscord(uid, did);
await BotDAO.linkDiscord(uid, did);
Logger.log("user_discord_link", `linked to ${did}`, uid);
return new MonkeyResponse("Discord account linked ", did);
await UsersDAO.linkDiscord(uid, discordId);
await BotDAO.linkDiscord(uid, discordId);
Logger.log("user_discord_link", `linked to ${discordId}`, uid);
return new MonkeyResponse("Discord account linked", discordId);
}
static async unlinkDiscord(req, _res) {
static async unlinkDiscord(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
let userInfo;
try {
userInfo = await UsersDAO.getUser(uid);
} catch (e) {
throw new MonkeyError(400, "User not found.");
}
const userInfo = await UsersDAO.getUser(uid);
if (!userInfo.discordId) {
throw new MonkeyError(400, "User does not have a linked Discord account");
}
await BotDAO.unlinkDiscord(uid, userInfo.discordId);
await UsersDAO.unlinkDiscord(uid);
Logger.log("user_discord_unlinked", userInfo.discordId, uid);
return new MonkeyResponse("Discord account unlinked ");
return new MonkeyResponse("Discord account unlinked");
}
static async addTag(req, _res) {
static async addTag(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const { tagName } = req.body;
let tag = await UsersDAO.addTag(uid, tagName);
const tag = await UsersDAO.addTag(uid, tagName);
return new MonkeyResponse("Tag updated", tag);
}
static async clearTagPb(req, _res) {
static async clearTagPb(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const { tagId } = req.params;
await UsersDAO.removeTagPb(uid, tagId);
[];
return new MonkeyResponse("Tag PB cleared");
}
static async editTag(req, _res) {
static async editTag(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const { tagId, newName } = req.body;
await UsersDAO.editTag(uid, tagId, newName);
return new MonkeyResponse("Tag updated");
}
static async removeTag(req, _res) {
static async removeTag(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const { tagId } = req.params;
await UsersDAO.removeTag(uid, tagId);
return new MonkeyResponse("Tag deleted");
}
static async getTags(req, _res) {
static async getTags(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
let tags = await UsersDAO.getTags(uid);
if (tags == undefined) tags = [];
return new MonkeyResponse("Tags retrieved", tags);
const tags = await UsersDAO.getTags(uid);
return new MonkeyResponse("Tags retrieved", tags ?? []);
}
static async updateLbMemory(req, _res) {
static async updateLbMemory(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const { mode, mode2, language, rank } = req.body;

View file

@ -4,6 +4,7 @@ import { Router } from "express";
import UserController from "../controllers/user";
import { asyncHandler, validateRequest } from "../../middlewares/api-utils";
import * as RateLimit from "../../middlewares/rate-limit";
import { isUsernameValid } from "../../handlers/validation";
const router = Router();
@ -18,6 +19,19 @@ const tagNameValidation = joi
"string.max": "Tag name exceeds maximum of 16 characters",
});
const usernameValidation = joi
.string()
.required()
.custom((value, helpers) => {
return isUsernameValid(value)
? value
: helpers.error("string.pattern.base");
})
.messages({
"string.pattern.base":
"Username invalid. Name cannot contain special characters or contain more than 14 characters. Can include _ . and -",
});
router.get(
"/",
RateLimit.userGet,
@ -32,7 +46,7 @@ router.post(
validateRequest({
body: {
email: joi.string().email(),
name: joi.string().required(),
name: usernameValidation,
uid: joi.string(),
},
}),
@ -44,7 +58,7 @@ router.get(
RateLimit.userCheckName,
validateRequest({
params: {
name: joi.string().required(),
name: usernameValidation,
},
}),
asyncHandler(UserController.checkName)
@ -63,7 +77,7 @@ router.patch(
authenticateRequest(),
validateRequest({
body: {
name: joi.string().required(),
name: usernameValidation,
},
}),
asyncHandler(UserController.updateName)

View file

@ -0,0 +1,23 @@
// Sorry for the bad words
const profanities = [
"miodec",
"bitly",
"fuck",
"bitch",
"shit",
"pussy",
"nigga",
"niqqa",
"niqqer",
"nigger",
"ni99a",
"ni99er",
"niggas",
"niga",
"niger",
"cunt",
"faggot",
"retard",
];
export default profanities;

View file

@ -1,4 +1,3 @@
// const MonkeyError = require("../handlers/error");
import db from "../init/db";
import { roundTo2 } from "../handlers/misc";

View file

@ -1,9 +1,11 @@
import _ from "lodash";
import { isUsernameValid } from "../handlers/validation";
import { updateAuthEmail } from "../handlers/auth";
import { checkAndUpdatePb } from "../handlers/pb";
import db from "../init/db";
import MonkeyError from "../handlers/error";
import { ObjectId } from "mongodb";
class UsersDAO {
static async addUser(name, email, uid) {
const user = await db.collection("users").findOne({ uid });
@ -75,11 +77,9 @@ class UsersDAO {
return user;
}
static async getUserByDiscordId(discordId) {
static async isDiscordIdAvailable(discordId) {
const user = await db.collection("users").findOne({ discordId });
if (!user)
throw new MonkeyError(404, "User not found", "get user by discord id");
return user;
return _.isNil(user);
}
static async addTag(uid, name) {

View file

@ -0,0 +1,34 @@
import fetch from "node-fetch";
const BASE_URL = "https://discord.com/api";
interface DiscordUser {
id: string;
username: string;
discriminator: string;
avatar?: string;
bot?: boolean;
system?: boolean;
mfa_enabled?: boolean;
banner?: string;
accent_color?: number;
locale?: string;
verified?: boolean;
email?: string;
flags?: number;
premium_type?: number;
public_flags?: number;
}
export async function linkAccount(
tokenType: string,
accessToken: string
): Promise<DiscordUser> {
const response = await fetch(`${BASE_URL}/users/@me`, {
headers: {
authorization: `${tokenType} ${accessToken}`,
},
});
return (await response.json()) as DiscordUser;
}

View file

@ -1,3 +1,5 @@
import uaparser from "ua-parser-js";
export function roundTo2(num) {
return Math.round((num + Number.EPSILON) * 100) / 100;
}
@ -44,3 +46,25 @@ export function base64UrlEncode(string) {
export function base64UrlDecode(string) {
return Buffer.from(string, "base64url").toString();
}
export function buildAgentLog(req) {
const agent = uaparser(req.headers["user-agent"]);
const agentLog = {
ip:
req.headers["cf-connecting-ip"] ||
req.headers["x-forwarded-for"] ||
req.ip ||
"255.255.255.255",
agent: `${agent.os.name} ${agent.os.version} ${agent.browser.name} ${agent.browser.version}`,
};
const {
device: { vendor, model, type },
} = agent;
if (vendor) {
agentLog.device = `${vendor} ${model} ${type}`;
}
return agentLog;
}

View file

@ -1,20 +0,0 @@
export function isUsernameValid(name) {
if (name === null || name === undefined || name === "") return false;
if (/.*miodec.*/.test(name.toLowerCase())) return false;
//sorry for the bad words
if (
/.*(bitly|fuck|bitch|shit|pussy|nigga|niqqa|niqqer|nigger|ni99a|ni99er|niggas|niga|niger|cunt|faggot|retard).*/.test(
name.toLowerCase()
)
)
return false;
if (name.length > 14) return false;
if (/^\..*/.test(name.toLowerCase())) return false;
return /^[0-9a-zA-Z_.-]+$/.test(name);
}
export function isTagPresetNameValid(name) {
if (name === null || name === undefined || name === "") return false;
if (name.length > 16) return false;
return /^[0-9a-zA-Z_.-]+$/.test(name);
}

View file

@ -0,0 +1,36 @@
import _ from "lodash";
import profanities from "../constants/profanities";
export function inRange(value: number, min: number, max: number): boolean {
return value >= min && value <= max;
}
export function isUsernameValid(name: string): boolean {
if (_.isNil(name) || !inRange(name.length, 1, 14)) {
return false;
}
const normalizedName = name.toLowerCase();
const beginsWithPeriod = /^\..*/.test(normalizedName);
if (beginsWithPeriod) {
return false;
}
const isProfanity = profanities.find((profanity) =>
normalizedName.includes(profanity)
);
if (isProfanity) {
return false;
}
return /^[0-9a-zA-Z_.-]+$/.test(name);
}
export function isTagPresetNameValid(name: string): boolean {
if (_.isNil(name) || !inRange(name.length, 1, 16)) {
return false;
}
return /^[0-9a-zA-Z_.-]+$/.test(name);
}

View file

@ -38,7 +38,9 @@
"@types/lodash": "4.14.178",
"@types/mongodb": "4.0.7",
"@types/node": "17.0.18",
"@types/node-fetch": "2.6.1",
"@types/swagger-stats": "0.95.4",
"@types/ua-parser-js": "0.7.36",
"@types/uuid": "8.3.4"
},
"engines": {
@ -749,6 +751,30 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA=="
},
"node_modules/@types/node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==",
"dev": true,
"dependencies": {
"@types/node": "*",
"form-data": "^3.0.0"
}
},
"node_modules/@types/node-fetch/node_modules/form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
@ -781,6 +807,12 @@
"prom-client": ">=11.5.3"
}
},
"node_modules/@types/ua-parser-js": {
"version": "0.7.36",
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz",
"integrity": "sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==",
"dev": true
},
"node_modules/@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
@ -5539,6 +5571,29 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA=="
},
"@types/node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==",
"dev": true,
"requires": {
"@types/node": "*",
"form-data": "^3.0.0"
},
"dependencies": {
"form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
@ -5571,6 +5626,12 @@
"prom-client": ">=11.5.3"
}
},
"@types/ua-parser-js": {
"version": "0.7.36",
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz",
"integrity": "sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==",
"dev": true
},
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",

View file

@ -45,7 +45,9 @@
"@types/lodash": "4.14.178",
"@types/mongodb": "4.0.7",
"@types/node": "17.0.18",
"@types/node-fetch": "2.6.1",
"@types/swagger-stats": "0.95.4",
"@types/ua-parser-js": "0.7.36",
"@types/uuid": "8.3.4"
}
}

View file

@ -62,6 +62,7 @@ declare namespace MonkeyTypes {
quoteMod?: boolean;
cannotReport?: boolean;
apeKeys?: Record<string, ApeKey>;
banned?: boolean;
}
interface ApeKey {