From 8b56eb5fbc3a2034598cdaae5b1e08198b19e8ac Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Tue, 5 Oct 2021 19:33:23 +0200 Subject: :sparkles: add User.register() method --- api/src/routes/auth/register.ts | 187 +++++++++++++--------------------------- util/src/entities/User.ts | 102 ++++++++++++++++++++-- 2 files changed, 153 insertions(+), 136 deletions(-) diff --git a/api/src/routes/auth/register.ts b/api/src/routes/auth/register.ts index 1344c994..c016c949 100644 --- a/api/src/routes/auth/register.ts +++ b/api/src/routes/auth/register.ts @@ -1,8 +1,8 @@ import { Request, Response, Router } from "express"; -import { trimSpecial, User, Snowflake, Config, defaultSettings, generateToken, Invite, adjustEmail } from "@fosscord/util"; -import bcrypt from "bcrypt"; -import { FieldErrors, route, getIpAdress, IPAnalysis, isProxy } from "@fosscord/api"; +import { Config, generateToken, Invite, FieldErrors, User, adjustEmail, trimSpecial } from "@fosscord/util"; +import { route, getIpAdress, IPAnalysis, isProxy } from "@fosscord/api"; import "missing-native-js-functions"; +import bcrypt from "bcrypt"; import { HTTPError } from "lambert-server"; const router: Router = Router(); @@ -34,22 +34,27 @@ export interface RegisterSchema { } router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => { - let { - email, - username, - password, - consent, - fingerprint, - invite, - date_of_birth, - gift_code_sku_id, // ? what is this - captcha_key - } = req.body; - - // get register Config + const body = req.body as RegisterSchema; const { register, security } = Config.get(); const ip = getIpAdress(req); + // email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick + let email = adjustEmail(body.email); + + // check if registration is allowed + if (!register.allowNewRegistration) { + throw FieldErrors({ + email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") } + }); + } + + // check if the user agreed to the Terms of Service + if (!body.consent) { + throw FieldErrors({ + consent: { code: "CONSENT_REQUIRED", message: req.t("auth:register.CONSENT_REQUIRED") } + }); + } + if (register.disabled) { throw FieldErrors({ email: { @@ -59,6 +64,33 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re }); } + if (register.requireCaptcha && security.captcha.enabled) { + if (!body.captcha_key) { + const { sitekey, service } = security.captcha; + return res?.status(400).json({ + captcha_key: ["captcha-required"], + captcha_sitekey: sitekey, + captcha_service: service + }); + } + + // TODO: check captcha + } + + if (!register.allowMultipleAccounts) { + // TODO: check if fingerprint was eligible generated + const exists = await User.findOne({ where: { fingerprints: body.fingerprint } }); + + if (exists) { + throw FieldErrors({ + email: { + code: "EMAIL_ALREADY_REGISTERED", + message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") + } + }); + } + } + if (register.blockProxies) { if (isProxy(await IPAnalysis(ip))) { console.log(`proxy ${ip} blocked from registration`); @@ -66,36 +98,15 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re } } - console.log("register", req.body.email, req.body.username, ip); + console.log("register", body.email, body.username, ip); // TODO: gift_code_sku_id? // TODO: check password strength - // email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick - email = adjustEmail(email); - - // trim special uf8 control characters -> Backspace, Newline, ... - username = trimSpecial(username); - - // discriminator will be randomly generated - let discriminator = ""; - - // check if registration is allowed - if (!register.allowNewRegistration) { - throw FieldErrors({ - email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") } - }); - } - - // check if the user agreed to the Terms of Service - if (!consent) { - throw FieldErrors({ - consent: { code: "CONSENT_REQUIRED", message: req.t("auth:register.CONSENT_REQUIRED") } - }); - } - if (email) { // replace all dots and chars after +, if its a gmail.com email - if (!email) throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req.t("auth:register.INVALID_EMAIL") } }); + if (!email) { + throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req?.t("auth:register.INVALID_EMAIL") } }); + } // check if there is already an account with this email const exists = await User.findOneOrFail({ email: email }).catch((e) => {}); @@ -114,17 +125,17 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re }); } - if (register.dateOfBirth.required && !date_of_birth) { + if (register.dateOfBirth.required && !body.date_of_birth) { throw FieldErrors({ date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } }); } else if (register.dateOfBirth.minimum) { const minimum = new Date(); minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum); - date_of_birth = new Date(date_of_birth); + body.date_of_birth = new Date(body.date_of_birth as Date); // higher is younger - if (date_of_birth > minimum) { + if (body.date_of_birth > minimum) { throw FieldErrors({ date_of_birth: { code: "DATE_OF_BIRTH_UNDERAGE", @@ -134,98 +145,20 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re } } - if (!register.allowMultipleAccounts) { - // TODO: check if fingerprint was eligible generated - const exists = await User.findOne({ where: { fingerprints: fingerprint } }); - - if (exists) { - throw FieldErrors({ - email: { - code: "EMAIL_ALREADY_REGISTERED", - message: req.t("auth:register.EMAIL_ALREADY_REGISTERED") - } - }); - } - } - - if (register.requireCaptcha && security.captcha.enabled) { - if (!captcha_key) { - const { sitekey, service } = security.captcha; - return res.status(400).json({ - captcha_key: ["captcha-required"], - captcha_sitekey: sitekey, - captcha_service: service - }); - } - - // TODO: check captcha - } - - if (password) { + if (body.password) { // the salt is saved in the password refer to bcrypt docs - password = await bcrypt.hash(password, 12); + body.password = await bcrypt.hash(body.password, 12); } else if (register.password.required) { throw FieldErrors({ password: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } }); } - let exists; - // randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists - // if it all five times already exists, abort with USERNAME_TOO_MANY_USERS error - // else just continue - // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the mongodb database? - for (let tries = 0; tries < 5; tries++) { - discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0"); - exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] }); - if (!exists) break; - } - - if (exists) { - throw FieldErrors({ - username: { - code: "USERNAME_TOO_MANY_USERS", - message: req.t("auth:register.USERNAME_TOO_MANY_USERS") - } - }); - } + const user = await User.register({ ...body, req }); - // TODO: save date_of_birth - // appearently discord doesn't save the date of birth and just calculate if nsfw is allowed - // if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false - - const user = await new User({ - created_at: new Date(), - username: username, - discriminator, - id: Snowflake.generate(), - bot: false, - system: false, - desktop: false, - mobile: false, - premium: true, - premium_type: 2, - bio: "", - mfa_enabled: false, - verified: true, - disabled: false, - deleted: false, - email: email, - rights: "0", - nsfw_allowed: true, // TODO: depending on age - public_flags: "0", - flags: "0", // TODO: generate - data: { - hash: password, - valid_tokens_since: new Date() - }, - settings: { ...defaultSettings, locale: req.language || "en-US" }, - fingerprints: [] - }).save(); - - if (invite) { + if (body.invite) { // await to fail if the invite doesn't exist (necessary for requireInvite to work properly) (username only signups are possible) - await Invite.joinGuild(user.id, invite); + await Invite.joinGuild(user.id, body.invite); } else if (register.requireInvite) { // require invite to register -> e.g. for organizations to send invites to their employees throw FieldErrors({ diff --git a/util/src/entities/User.ts b/util/src/entities/User.ts index b6f3723c..a139d362 100644 --- a/util/src/entities/User.ts +++ b/util/src/entities/User.ts @@ -3,6 +3,8 @@ import { BaseClass } from "./BaseClass"; import { BitField } from "../util/BitField"; import { Relationship } from "./Relationship"; import { ConnectedAccount } from "./ConnectedAccount"; +import { Config, FieldErrors, Snowflake, trimSpecial } from ".."; +import { Member } from "."; export enum PublicUserEnum { username, @@ -74,13 +76,13 @@ export class User extends BaseClass { @Column({ nullable: true }) banner?: string; // hash of the user banner - @Column({ nullable: true }) + @Column({ nullable: true, select: false }) phone?: string; // phone number of the user - @Column() + @Column({ select: false }) desktop: boolean; // if the user has desktop app installed - @Column() + @Column({ select: false }) mobile: boolean; // if the user has mobile app installed @Column() @@ -98,16 +100,16 @@ export class User extends BaseClass { @Column() system: boolean; // shouldn't be used, the api sents this field type true, if the generated message comes from a system generated author - @Column() + @Column({ select: false }) nsfw_allowed: boolean; // if the user is older than 18 (resp. Config) - @Column() + @Column({ select: false }) mfa_enabled: boolean; // if multi factor authentication is enabled @Column() created_at: Date; // registration date - @Column() + @Column({ select: false }) verified: boolean; // if the user is offically verified @Column() @@ -116,7 +118,7 @@ export class User extends BaseClass { @Column() deleted: boolean; // if the user was deleted - @Column({ nullable: true }) + @Column({ nullable: true, select: false }) email?: string; // email of the user @Column() @@ -148,10 +150,10 @@ export class User extends BaseClass { hash?: string; // hash of the password, salt is saved in password (bcrypt) }; - @Column({ type: "simple-array" }) + @Column({ type: "simple-array", select: false }) fingerprints: string[]; // array of fingerprints -> used to prevent multiple accounts - @Column({ type: "simple-json" }) + @Column({ type: "simple-json", select: false }) settings: UserSettings; toPublicUser() { @@ -171,6 +173,88 @@ export class User extends BaseClass { } ); } + + static async register({ + email, + username, + password, + date_of_birth, + req, + }: { + username: string; + password?: string; + email?: string; + date_of_birth?: Date; // "2000-04-03" + req?: any; + }) { + // trim special uf8 control characters -> Backspace, Newline, ... + username = trimSpecial(username); + + // discriminator will be randomly generated + let discriminator = ""; + + let exists; + // randomly generates a discriminator between 1 and 9999 and checks max five times if it already exists + // if it all five times already exists, abort with USERNAME_TOO_MANY_USERS error + // else just continue + // TODO: is there any better way to generate a random discriminator only once, without checking if it already exists in the mongodb database? + for (let tries = 0; tries < 5; tries++) { + discriminator = Math.randomIntBetween(1, 9999).toString().padStart(4, "0"); + exists = await User.findOne({ where: { discriminator, username: username }, select: ["id"] }); + if (!exists) break; + } + + if (exists) { + throw FieldErrors({ + username: { + code: "USERNAME_TOO_MANY_USERS", + message: req.t("auth:register.USERNAME_TOO_MANY_USERS"), + }, + }); + } + + // TODO: save date_of_birth + // appearently discord doesn't save the date of birth and just calculate if nsfw is allowed + // if nsfw_allowed is null/undefined it'll require date_of_birth to set it to true/false + const language = req.language === "en" ? "en-US" : req.language || "en-US"; + + const user = await new User({ + created_at: new Date(), + username: username, + discriminator, + id: Snowflake.generate(), + bot: false, + system: false, + desktop: false, + mobile: false, + premium: true, + premium_type: 2, + bio: "", + mfa_enabled: false, + verified: true, + disabled: false, + deleted: false, + email: email, + rights: "0", + nsfw_allowed: true, // TODO: depending on age + public_flags: "0", + flags: "0", // TODO: generate + data: { + hash: password, + valid_tokens_since: new Date(), + }, + settings: { ...defaultSettings, locale: language }, + fingerprints: [], + }).save(); + + if (Config.get().guild.autoJoin.enabled) { + for (const guild of Config.get().guild.autoJoin.guilds || []) { + await Member.addToGuild(user.id, guild); + } + } + + return user; + } } export const defaultSettings: UserSettings = { -- cgit 1.4.1