summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-10-05 19:33:23 +0200
committerFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-10-05 19:33:23 +0200
commit9cf018d737834836159e97a5b7f74f5105e481b7 (patch)
treedcd864326c1c7f46c80a0b4eace402daf88ad5ca
parent:arrow_up: update mnjsf (diff)
downloadserver-9cf018d737834836159e97a5b7f74f5105e481b7.tar.xz
:sparkles: add User.register() method
-rw-r--r--api/src/routes/auth/register.ts187
-rw-r--r--util/src/entities/User.ts102
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 = {