✨ add User.register() method
This commit is contained in:
		
							parent
							
								
									2bd5c06bf1
								
							
						
					
					
						commit
						9cf018d737
					
				| @ -1,8 +1,8 @@ | |||||||
| import { Request, Response, Router } from "express"; | import { Request, Response, Router } from "express"; | ||||||
| import { trimSpecial, User, Snowflake, Config, defaultSettings, generateToken, Invite, adjustEmail } from "@fosscord/util"; | import { Config, generateToken, Invite, FieldErrors, User, adjustEmail, trimSpecial } from "@fosscord/util"; | ||||||
| import bcrypt from "bcrypt"; | import { route, getIpAdress, IPAnalysis, isProxy } from "@fosscord/api"; | ||||||
| import { FieldErrors, route, getIpAdress, IPAnalysis, isProxy } from "@fosscord/api"; |  | ||||||
| import "missing-native-js-functions"; | import "missing-native-js-functions"; | ||||||
|  | import bcrypt from "bcrypt"; | ||||||
| import { HTTPError } from "lambert-server"; | import { HTTPError } from "lambert-server"; | ||||||
| 
 | 
 | ||||||
| const router: Router = Router(); | const router: Router = Router(); | ||||||
| @ -34,22 +34,27 @@ export interface RegisterSchema { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => { | router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => { | ||||||
| 	let { | 	const body = req.body as RegisterSchema; | ||||||
| 		email, |  | ||||||
| 		username, |  | ||||||
| 		password, |  | ||||||
| 		consent, |  | ||||||
| 		fingerprint, |  | ||||||
| 		invite, |  | ||||||
| 		date_of_birth, |  | ||||||
| 		gift_code_sku_id, // ? what is this
 |  | ||||||
| 		captcha_key |  | ||||||
| 	} = req.body; |  | ||||||
| 
 |  | ||||||
| 	// get register Config
 |  | ||||||
| 	const { register, security } = Config.get(); | 	const { register, security } = Config.get(); | ||||||
| 	const ip = getIpAdress(req); | 	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) { | 	if (register.disabled) { | ||||||
| 		throw FieldErrors({ | 		throw FieldErrors({ | ||||||
| 			email: { | 			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 (register.blockProxies) { | ||||||
| 		if (isProxy(await IPAnalysis(ip))) { | 		if (isProxy(await IPAnalysis(ip))) { | ||||||
| 			console.log(`proxy ${ip} blocked from registration`); | 			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: gift_code_sku_id?
 | ||||||
| 	// TODO: check password strength
 | 	// 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) { | 	if (email) { | ||||||
| 		// replace all dots and chars after +, if its a gmail.com 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
 | 		// check if there is already an account with this email
 | ||||||
| 		const exists = await User.findOneOrFail({ email: email }).catch((e) => {}); | 		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({ | 		throw FieldErrors({ | ||||||
| 			date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } | 			date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } | ||||||
| 		}); | 		}); | ||||||
| 	} else if (register.dateOfBirth.minimum) { | 	} else if (register.dateOfBirth.minimum) { | ||||||
| 		const minimum = new Date(); | 		const minimum = new Date(); | ||||||
| 		minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum); | 		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
 | 		// higher is younger
 | ||||||
| 		if (date_of_birth > minimum) { | 		if (body.date_of_birth > minimum) { | ||||||
| 			throw FieldErrors({ | 			throw FieldErrors({ | ||||||
| 				date_of_birth: { | 				date_of_birth: { | ||||||
| 					code: "DATE_OF_BIRTH_UNDERAGE", | 					code: "DATE_OF_BIRTH_UNDERAGE", | ||||||
| @ -134,98 +145,20 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if (!register.allowMultipleAccounts) { | 	if (body.password) { | ||||||
| 		// 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) { |  | ||||||
| 		// the salt is saved in the password refer to bcrypt docs
 | 		// 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) { | 	} else if (register.password.required) { | ||||||
| 		throw FieldErrors({ | 		throw FieldErrors({ | ||||||
| 			password: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } | 			password: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") } | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	let exists; | 	const user = await User.register({ ...body, req }); | ||||||
| 	// 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) { | 	if (body.invite) { | ||||||
| 		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 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) { |  | ||||||
| 		// await to fail if the invite doesn't exist (necessary for requireInvite to work properly) (username only signups are possible)
 | 		// 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) { | 	} else if (register.requireInvite) { | ||||||
| 		// require invite to register -> e.g. for organizations to send invites to their employees
 | 		// require invite to register -> e.g. for organizations to send invites to their employees
 | ||||||
| 		throw FieldErrors({ | 		throw FieldErrors({ | ||||||
|  | |||||||
| @ -3,6 +3,8 @@ import { BaseClass } from "./BaseClass"; | |||||||
| import { BitField } from "../util/BitField"; | import { BitField } from "../util/BitField"; | ||||||
| import { Relationship } from "./Relationship"; | import { Relationship } from "./Relationship"; | ||||||
| import { ConnectedAccount } from "./ConnectedAccount"; | import { ConnectedAccount } from "./ConnectedAccount"; | ||||||
|  | import { Config, FieldErrors, Snowflake, trimSpecial } from ".."; | ||||||
|  | import { Member } from "."; | ||||||
| 
 | 
 | ||||||
| export enum PublicUserEnum { | export enum PublicUserEnum { | ||||||
| 	username, | 	username, | ||||||
| @ -74,13 +76,13 @@ export class User extends BaseClass { | |||||||
| 	@Column({ nullable: true }) | 	@Column({ nullable: true }) | ||||||
| 	banner?: string; // hash of the user banner
 | 	banner?: string; // hash of the user banner
 | ||||||
| 
 | 
 | ||||||
| 	@Column({ nullable: true }) | 	@Column({ nullable: true, select: false }) | ||||||
| 	phone?: string; // phone number of the user
 | 	phone?: string; // phone number of the user
 | ||||||
| 
 | 
 | ||||||
| 	@Column() | 	@Column({ select: false }) | ||||||
| 	desktop: boolean; // if the user has desktop app installed
 | 	desktop: boolean; // if the user has desktop app installed
 | ||||||
| 
 | 
 | ||||||
| 	@Column() | 	@Column({ select: false }) | ||||||
| 	mobile: boolean; // if the user has mobile app installed
 | 	mobile: boolean; // if the user has mobile app installed
 | ||||||
| 
 | 
 | ||||||
| 	@Column() | 	@Column() | ||||||
| @ -98,16 +100,16 @@ export class User extends BaseClass { | |||||||
| 	@Column() | 	@Column() | ||||||
| 	system: boolean; // shouldn't be used, the api sents this field type true, if the generated message comes from a system generated author
 | 	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)
 | 	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
 | 	mfa_enabled: boolean; // if multi factor authentication is enabled
 | ||||||
| 
 | 
 | ||||||
| 	@Column() | 	@Column() | ||||||
| 	created_at: Date; // registration date
 | 	created_at: Date; // registration date
 | ||||||
| 
 | 
 | ||||||
| 	@Column() | 	@Column({ select: false }) | ||||||
| 	verified: boolean; // if the user is offically verified
 | 	verified: boolean; // if the user is offically verified
 | ||||||
| 
 | 
 | ||||||
| 	@Column() | 	@Column() | ||||||
| @ -116,7 +118,7 @@ export class User extends BaseClass { | |||||||
| 	@Column() | 	@Column() | ||||||
| 	deleted: boolean; // if the user was deleted
 | 	deleted: boolean; // if the user was deleted
 | ||||||
| 
 | 
 | ||||||
| 	@Column({ nullable: true }) | 	@Column({ nullable: true, select: false }) | ||||||
| 	email?: string; // email of the user
 | 	email?: string; // email of the user
 | ||||||
| 
 | 
 | ||||||
| 	@Column() | 	@Column() | ||||||
| @ -148,10 +150,10 @@ export class User extends BaseClass { | |||||||
| 		hash?: string; // hash of the password, salt is saved in password (bcrypt)
 | 		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
 | 	fingerprints: string[]; // array of fingerprints -> used to prevent multiple accounts
 | ||||||
| 
 | 
 | ||||||
| 	@Column({ type: "simple-json" }) | 	@Column({ type: "simple-json", select: false }) | ||||||
| 	settings: UserSettings; | 	settings: UserSettings; | ||||||
| 
 | 
 | ||||||
| 	toPublicUser() { | 	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 = { | export const defaultSettings: UserSettings = { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user