diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts index 79929f5..1427b3c 100644 --- a/src/api/routes/auth/register.ts +++ b/src/api/routes/auth/register.ts @@ -17,7 +17,7 @@ */ import { route, verifyCaptcha } from "@spacebar/api"; -import { Config, FieldErrors, Invite, User, ValidRegistrationToken, generateToken, IpDataClient, AbuseIpDbClient, TimeSpan } from "@spacebar/util"; +import { Config, FieldErrors, Invite, User, ValidRegistrationToken, generateToken, IpDataClient, AbuseIpDbClient, TimeSpan, Stopwatch } from "@spacebar/util"; import bcrypt from "bcrypt"; import { Request, Response, Router } from "express"; import { HTTPError } from "lambert-server"; @@ -45,6 +45,13 @@ }, }), async (req: Request, res: Response) => { + const totalSw = Stopwatch.startNew(); + const incSw = Stopwatch.startNew(); + const logTrace = (...data: unknown[]) => { + if (process.env.LOG_VERBOSE_TRACES !== "true") return; + console.log("[Register]", ...data, `[${totalSw.elapsed().toString()} (+${incSw.getElapsedAndReset().totalMilliseconds}ms)]`); + }; + const body = req.body as RegisterSchema; const { register, security, limits } = Config.get(); const ip = req.ip!; @@ -133,6 +140,8 @@ } } + logTrace("Basic checks"); + //region IP checks const cacheBlockedIp = (ip: string, reason: string) => { recentlyBlockedIps[ip] = { @@ -206,6 +215,7 @@ } } //endregion + logTrace("IP checks"); // TODO: gift_code_sku_id? // TODO: check password strength @@ -242,6 +252,8 @@ }); } + logTrace("Email checks"); + if (register.dateOfBirth.required && !body.date_of_birth) { throw FieldErrors({ date_of_birth: { @@ -302,6 +314,7 @@ }, }); } + logTrace("Password checks"); if (!regTokenUsed && !body.invite && (register.requireInvite || (register.guestsRequireInvite && !register.email))) { // require invite to register -> e.g. for organizations to send invites to their employees @@ -330,6 +343,7 @@ }, }); } + logTrace("Absolute register rate checks"); const { maxUsername } = Config.get().limits.user; if (body.username.length > maxUsername) { @@ -342,13 +356,16 @@ } const user = await User.register({ ...body, req }); + logTrace("Register user"); 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, body.invite); + logTrace("Accept invite"); } - return res.json({ token: await generateToken(user.id) }); + res.json({ token: await generateToken(user.id) }); + logTrace("Generate token"); }, ); diff --git a/src/util/entities/Member.ts b/src/util/entities/Member.ts index 8a460af..fba7c40 100644 --- a/src/util/entities/Member.ts +++ b/src/util/entities/Member.ts @@ -21,7 +21,7 @@ import { Ban, Channel, PublicGuildRelations } from "."; import { ReadyGuildDTO } from "../dtos"; import { GuildCreateEvent, GuildDeleteEvent, GuildMemberAddEvent, GuildMemberRemoveEvent, GuildMemberUpdateEvent, MessageCreateEvent } from "../interfaces"; -import { Config, emitEvent, DiscordApiErrors } from "../util"; +import { Config, emitEvent, DiscordApiErrors, Stopwatch } from "../util"; import { BaseClassWithoutId } from "./BaseClass"; import { Guild } from "./Guild"; import { Message } from "./Message"; @@ -305,16 +305,26 @@ } static async addToGuild(user_id: string, guild_id: string) { + const totalSw = Stopwatch.startNew(); + const incSw = Stopwatch.startNew(); + const logTrace = (...data: unknown[]) => { + if (process.env.LOG_VERBOSE_TRACES !== "true") return; + console.log("[Member.addToGuild]", ...data, `[${totalSw.elapsed().toString()} (+${incSw.getElapsedAndReset().totalMilliseconds}ms)]`); + }; + const user = await User.getPublicUser(user_id); - const isBanned = await Ban.count({ where: { guild_id, user_id } }); - if (isBanned) { - throw DiscordApiErrors.USER_BANNED; - } + logTrace("Get user"); + + const isBanned = await Ban.findOne({ where: { guild_id, user_id }, select: { id: true } }); + if (isBanned) throw DiscordApiErrors.USER_BANNED; + logTrace("Check ban"); + const { maxGuilds } = Config.get().limits.user; const guild_count = await Member.count({ where: { id: user_id } }); if (guild_count >= maxGuilds) { throw new HTTPError(`You are at the ${maxGuilds} server limit.`, 403); } + logTrace("Enforce max guilds"); const guild = await Guild.findOneOrFail({ where: { @@ -323,12 +333,15 @@ relations: PublicGuildRelations, relationLoadStrategy: "query", }); + logTrace("Find guild"); for await (const channel of guild.channels) { channel.position = await Channel.calculatePosition(channel.id, guild_id); } + logTrace("Reorder channels"); const memberCount = await Member.count({ where: { guild_id } }); + logTrace("Get member count"); const memberPreview = ( await Member.find({ @@ -344,6 +357,7 @@ take: 10, }) ).map((member) => member.toPublicMember()); + logTrace("Calculate member preview"); if ( await Member.count({ @@ -351,6 +365,7 @@ }) ) throw new HTTPError("You are already a member of this guild", 400); + logTrace("Check existing membership"); const member = { id: user_id, @@ -417,6 +432,7 @@ user_id, } satisfies GuildCreateEvent), ]); + logTrace("Save member info"); if (guild.system_channel_id) { const channel = await Channel.findOneOrFail({ @@ -452,6 +468,7 @@ } satisfies MessageCreateEvent), channel.save(), ]); + logTrace("Send welcome message"); } } diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts index 249d095..a58b657 100644 --- a/src/util/entities/User.ts +++ b/src/util/entities/User.ts @@ -18,7 +18,7 @@ import { Request } from "express"; import { Column, Entity, JoinColumn, OneToMany, OneToOne } from "typeorm"; -import { Channel, Config, Email, FieldErrors, Snowflake, trimSpecial } from ".."; +import { Channel, Config, Email, FieldErrors, Snowflake, Stopwatch, trimSpecial } from ".."; import { Random } from "../util"; import { BaseClass } from "./BaseClass"; import { ConnectedAccount } from "./ConnectedAccount"; @@ -296,6 +296,13 @@ req?: Request; bot?: boolean; }) { + const totalSw = Stopwatch.startNew(); + const incSw = Stopwatch.startNew(); + const logTrace = (...data: unknown[]) => { + if (process.env.LOG_VERBOSE_TRACES !== "true") return; + console.log("[User.register]", ...data, `[${totalSw.elapsed().toString()} (+${incSw.getElapsedAndReset().totalMilliseconds}ms)]`); + }; + // trim special utf8 control characters -> Backspace, Newline, ... username = trimSpecial(username); @@ -309,6 +316,7 @@ }, }); } + logTrace("Generate discriminator"); // TODO: save date_of_birth // apparently discord doesn't save the date of birth and just calculate if nsfw is allowed @@ -340,20 +348,24 @@ }); user.validate(); + logTrace("Generate/validate user"); + await Promise.all([user.save(), settings.save()]); + logTrace("Save user"); // send verification email if users aren't verified by default and we have an email if (!Config.get().defaults.user.verified && email) { await Email.sendVerifyEmail(user, email).catch((e) => { console.error(`Failed to send verification email to ${user.tag}: ${e}`); }); + logTrace("Send verify email"); } - const { guild } = Config.get(); - if (guild.autoJoin.enabled && !(bot && !guild.autoJoin.bots)) - for (const guild of Config.get().guild.autoJoin.guilds || []) { - await Member.addToGuild(user.id, guild).catch((e) => console.error("[Autojoin]", e)); - } + const { autoJoin } = Config.get().guild; + if (autoJoin.enabled && autoJoin.guilds.length > 0 && !(bot && !autoJoin.bots)) { + await Promise.all(autoJoin.guilds.map((guild) => Member.addToGuild(user.id, guild).catch((e) => console.error("[Autojoin]", e)))); + logTrace("Autojoin", autoJoin.guilds.length, "guilds"); + } return user; }