mirror of
https://github.com/spacebarchat/server.git
synced 2024-11-10 20:52:42 +01:00
✨ 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…
Reference in New Issue
Block a user