summary refs log tree commit diff
diff options
context:
space:
mode:
authorTheArcaneBrony <myrainbowdash949@gmail.com>2022-09-20 16:43:35 +0200
committerGitHub <noreply@github.com>2022-09-20 16:43:35 +0200
commit313959557df08b03ae07a85c6faafa9817a9f696 (patch)
tree8e3c1e5159f0b1ee0eba3c9f69a15d7894430726
parentMerge pull request #885 from fosscord/dev/Maddy/fix/genSchemas (diff)
parentCourtesy: run prettier (diff)
downloadserver-313959557df08b03ae07a85c6faafa9817a9f696.tar.xz
Merge pull request #891 from fosscord/dev/improve-security
Improved security: one-time registration token support, register and message ratelimit
-rw-r--r--.gitignore2
-rw-r--r--.prettierignore14
-rw-r--r--assets/locales/en/auth.json7
-rw-r--r--assets/locales/en/common.json31
-rw-r--r--src/api/middlewares/Authentication.ts2
-rw-r--r--src/api/middlewares/BodyParser.ts2
-rw-r--r--src/api/middlewares/RateLimit.ts3
-rw-r--r--src/api/routes/auth/generate-registration-tokens.ts27
-rw-r--r--src/api/routes/auth/location-metadata.ts3
-rw-r--r--src/api/routes/auth/login.ts4
-rw-r--r--src/api/routes/auth/logout.ts16
-rw-r--r--src/api/routes/auth/register.ts137
-rw-r--r--src/api/routes/channels/#channel_id/invites.ts4
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts6
-rw-r--r--src/api/routes/channels/#channel_id/messages/index.ts46
-rw-r--r--src/api/routes/channels/#channel_id/permissions.ts8
-rw-r--r--src/api/routes/channels/#channel_id/webhooks.ts2
-rw-r--r--src/api/routes/guilds/#guild_id/bans.ts3
-rw-r--r--src/api/routes/guilds/#guild_id/regions.ts4
-rw-r--r--src/api/routes/guilds/#guild_id/stickers.ts2
-rw-r--r--src/api/routes/guilds/#guild_id/templates.ts4
-rw-r--r--src/api/routes/guilds/#guild_id/widget.json.ts4
-rw-r--r--src/api/routes/users/@me/relationships.ts19
-rw-r--r--src/api/routes/voice/regions.ts3
-rw-r--r--src/api/util/handlers/Voice.ts3
-rw-r--r--src/api/util/index.ts6
-rw-r--r--src/api/util/utility/String.ts18
-rw-r--r--src/cdn/routes/attachments.ts10
-rw-r--r--src/cdn/routes/avatars.ts12
-rw-r--r--src/cdn/routes/external.ts5
-rw-r--r--src/cdn/routes/guild-profiles.ts12
-rw-r--r--src/cdn/routes/role-icons.ts12
-rw-r--r--src/util/config/types/GeneralConfiguration.ts1
-rw-r--r--src/util/config/types/LimitConfigurations.ts3
-rw-r--r--src/util/config/types/RegisterConfiguration.ts1
-rw-r--r--src/util/config/types/SecurityConfiguration.ts1
-rw-r--r--src/util/config/types/subconfigurations/limits/GlobalRateLimits.ts10
-rw-r--r--src/util/config/types/subconfigurations/limits/index.ts1
-rw-r--r--src/util/entities/Attachment.ts2
-rw-r--r--src/util/entities/Invite.ts2
-rw-r--r--src/util/entities/ValidRegistrationTokens.ts12
-rw-r--r--src/util/entities/index.ts1
-rw-r--r--src/util/migrations/mariadb/1663440589234-registration_tokens.ts30
-rw-r--r--src/util/migrations/mariadb/1663448562034-drop_id_for_registration_tokens.ts32
-rw-r--r--src/util/migrations/postgres/1663440587650-registration_tokens.ts32
-rw-r--r--src/util/migrations/postgres/1663448561249-drop_id_for_registration_tokens.ts32
-rw-r--r--src/util/migrations/sqlite/1663440585960-registration_tokens.ts245
-rw-r--r--src/util/migrations/sqlite/1663448560501-drop_id_for_registration_tokens.ts96
-rw-r--r--src/util/util/Base64.ts (renamed from src/api/util/utility/Base64.ts)0
-rw-r--r--src/util/util/CDN.ts (renamed from src/util/util/cdn.ts)0
-rw-r--r--src/util/util/Captcha.ts (renamed from src/api/util/utility/captcha.ts)0
-rw-r--r--src/util/util/IPAddress.ts (renamed from src/api/util/utility/ipAddress.ts)0
-rw-r--r--src/util/util/PasswordStrength.ts (renamed from src/api/util/utility/passwordStrength.ts)0
-rw-r--r--src/util/util/RandomInviteID.ts (renamed from src/api/util/utility/RandomInviteID.ts)6
-rw-r--r--src/util/util/String.ts18
-rw-r--r--src/util/util/index.ts5
56 files changed, 824 insertions, 137 deletions
diff --git a/.gitignore b/.gitignore
index a582a2f3..8d2feb42 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,3 +32,5 @@ yarn.lock
 dbconf.json
 
 migrations.db
+
+package-lock.json
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 00000000..036ed868
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,14 @@
+.DS_STORE
+db/
+dist/
+node_modules
+tsconfig.tsbuildinfo
+*.log
+*.log.ansi
+bundle/depclean.*
+*.tmp
+tmp/
+assets/cache/
+*.generated
+.yarn/
+yarn.lock
\ No newline at end of file
diff --git a/assets/locales/en/auth.json b/assets/locales/en/auth.json
index b6264a43..dc4358f1 100644
--- a/assets/locales/en/auth.json
+++ b/assets/locales/en/auth.json
@@ -14,6 +14,11 @@
 		"DATE_OF_BIRTH_UNDERAGE": "You need to be {{years}} years or older",
 		"CONSENT_REQUIRED": "You must agree to the Terms of Service and Privacy Policy.",
 		"USERNAME_TOO_MANY_USERS": "Too many users have this username, please try another",
-		"GUESTS_DISABLED": "Guest users are disabled"
+		"GUESTS_DISABLED": "Guest users are disabled",
+		"TOO_MANY_REGISTRATIONS": "Too many registrations, please try again later",
+		"IP_BLOCKED": "Your IP is blocked from registration"
+	},
+	"generic": {
+		"MISSING_AUTH_HEADER": "Missing Authorization Header"
 	}
 }
diff --git a/assets/locales/en/common.json b/assets/locales/en/common.json
index 8bb9c042..edcafa1d 100644
--- a/assets/locales/en/common.json
+++ b/assets/locales/en/common.json
@@ -14,5 +14,36 @@
 		"EMAIL_TYPE_INVALID_EMAIL": "Not a well-formed email address",
 		"DATE_TYPE_PARSE": "Could not parse {{date}}. Should be ISO8601",
 		"BASE_TYPE_BAD_LENGTH": "Must be between {{length}} in length"
+	},
+	"body": {
+		"INVALID_BODY": "Invalid Body",
+		"INVALID_REQUEST_SIGNATURE": "Invalid request signature",
+		"MISSING_FILE": "File missing",
+		"INVALID_FILE_TYPE": "Invalid file type"
+	},
+	"notfound": {
+		"CHANNEL": "This channel doesn't exist",
+		"USER": "User not found",
+		"ROLE": "Role not found",
+		"REACTION": "Reaction not found",
+		"FILE": "File not found"
+	},
+	"toomany": {
+		"CHANNEL": "Too many channels",
+		"USER": "Too many users",
+		"ROLE": "Too many roles",
+		"REACTION": "Too many reactions",
+		"FILE": "Too many files",
+		"MESSAGE": "Too many messages"
+	},
+	"relationship": {
+		"ALREADY_BLOCKED": "You already blocked the user",
+		"NOT_FRIENDS": "You are not friends with the user",
+		"ALREADY_FRIENDS": "You are already friends with the user",
+		"ALREADY_SENT": "You already sent a friend request",
+		"ADD_SELF": "You can't add yourself as a friend",
+		"REMOVE_SELF": "You can't remove yourself as a friend",
+		"UNBLOCK": "Unblock the user before sending a friend request",
+		"BLOCKED": "The user blocked you"
 	}
 }
diff --git a/src/api/middlewares/Authentication.ts b/src/api/middlewares/Authentication.ts
index 6d063953..00c2e5e6 100644
--- a/src/api/middlewares/Authentication.ts
+++ b/src/api/middlewares/Authentication.ts
@@ -53,7 +53,7 @@ export async function Authentication(req: Request, res: Response, next: NextFunc
 		})
 	)
 		return next();
-	if (!req.headers.authorization) return next(new HTTPError("Missing Authorization Header", 401));
+	if (!req.headers.authorization) return next(new HTTPError("Missing authorization header!", 401));
 
 	try {
 		const { jwtSecret } = Config.get().security;
diff --git a/src/api/middlewares/BodyParser.ts b/src/api/middlewares/BodyParser.ts
index 36d89da7..b7c99638 100644
--- a/src/api/middlewares/BodyParser.ts
+++ b/src/api/middlewares/BodyParser.ts
@@ -11,7 +11,7 @@ export function BodyParser(opts?: OptionsJson) {
 		jsonParser(req, res, (err) => {
 			if (err) {
 				// TODO: different errors for body parser (request size limit, wrong body type, invalid body, ...)
-				return next(new HTTPError("Invalid Body", 400));
+				return next(new HTTPError(req.t("common:body.INVALID_BODY"), 400));
 			}
 			next();
 		});
diff --git a/src/api/middlewares/RateLimit.ts b/src/api/middlewares/RateLimit.ts
index dc93dcef..b19f9f7e 100644
--- a/src/api/middlewares/RateLimit.ts
+++ b/src/api/middlewares/RateLimit.ts
@@ -1,5 +1,4 @@
-import { getIpAdress } from "@fosscord/api";
-import { Config, getRights, listenEvent } from "@fosscord/util";
+import { Config, getIpAdress, getRights, listenEvent } from "@fosscord/util";
 import { NextFunction, Request, Response, Router } from "express";
 import { API_PREFIX_TRAILING_SLASH } from "./Authentication";
 
diff --git a/src/api/routes/auth/generate-registration-tokens.ts b/src/api/routes/auth/generate-registration-tokens.ts
new file mode 100644
index 00000000..6f7f8630
--- /dev/null
+++ b/src/api/routes/auth/generate-registration-tokens.ts
@@ -0,0 +1,27 @@
+import { route } from "@fosscord/api";
+import { Config, random, ValidRegistrationToken } from "@fosscord/util";
+import { Request, Response, Router } from "express";
+
+const router: Router = Router();
+export default router;
+
+router.get("/", route({ right: "OPERATOR" }), async (req: Request, res: Response) => {
+	let count = (req.query.count as unknown as number) ?? 1;
+	let tokens: string[] = [];
+	let dbtokens: ValidRegistrationToken[] = [];
+	for (let i = 0; i < count; i++) {
+		let token = random((req.query.length as unknown as number) ?? 255);
+		let vrt = new ValidRegistrationToken();
+		vrt.token = token;
+		dbtokens.push(vrt);
+		if (req.query.include_url == "true") token = `${Config.get().general.publicUrl}/register?token=${token}`;
+		tokens.push(token);
+	}
+	await ValidRegistrationToken.save(dbtokens, { chunk: 1000, reload: false, transaction: false });
+
+	if (req.query.plain == "true") {
+		if (count == 1) res.send(tokens[0]);
+		else res.send(tokens.join("\n"));
+	} else if (count == 1) res.json({ token: tokens[0] });
+	else res.json({ tokens });
+});
diff --git a/src/api/routes/auth/location-metadata.ts b/src/api/routes/auth/location-metadata.ts
index b8caf579..5ccd7e85 100644
--- a/src/api/routes/auth/location-metadata.ts
+++ b/src/api/routes/auth/location-metadata.ts
@@ -1,4 +1,5 @@
-import { getIpAdress, IPAnalysis, route } from "@fosscord/api";
+import { route } from "@fosscord/api";
+import { getIpAdress, IPAnalysis } from "@fosscord/util";
 import { Request, Response, Router } from "express";
 const router = Router();
 
diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts
index 045b86eb..4c882c14 100644
--- a/src/api/routes/auth/login.ts
+++ b/src/api/routes/auth/login.ts
@@ -1,5 +1,5 @@
-import { getIpAdress, route, verifyCaptcha } from "@fosscord/api";
-import { adjustEmail, Config, FieldErrors, generateToken, LoginSchema, User } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { adjustEmail, Config, FieldErrors, generateToken, getIpAdress, LoginSchema, User, verifyCaptcha } from "@fosscord/util";
 import crypto from "crypto";
 import { Request, Response, Router } from "express";
 
diff --git a/src/api/routes/auth/logout.ts b/src/api/routes/auth/logout.ts
new file mode 100644
index 00000000..7e36ae9a
--- /dev/null
+++ b/src/api/routes/auth/logout.ts
@@ -0,0 +1,16 @@
+import { route } from "@fosscord/api";
+import { Request, Response, Router } from "express";
+
+const router: Router = Router();
+export default router;
+
+router.post("/", route({}), async (req: Request, res: Response) => {
+	if (req.body.provider != null || req.body.voip_provider != null) {
+		console.log(`[LOGOUT]: provider or voip provider not null!`, req.body);
+	} else {
+		delete req.body.provider;
+		delete req.body.voip_provider;
+		if (Object.keys(req.body).length != 0) console.log(`[LOGOUT]: Extra fields sent in logout!`, req.body);
+	}
+	res.status(204).send();
+});
diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts
index 5cc28f7a..50f89522 100644
--- a/src/api/routes/auth/register.ts
+++ b/src/api/routes/auth/register.ts
@@ -1,6 +1,22 @@
-import { getIpAdress, IPAnalysis, isProxy, route, verifyCaptcha } from "@fosscord/api";
-import { adjustEmail, Config, FieldErrors, generateToken, HTTPError, Invite, RegisterSchema, User } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import {
+	adjustEmail,
+	Config,
+	FieldErrors,
+	generateToken,
+	getIpAdress,
+	HTTPError,
+	Invite,
+	IPAnalysis,
+	isProxy,
+	RegisterSchema,
+	User,
+	ValidRegistrationToken,
+	verifyCaptcha
+} from "@fosscord/util";
 import { Request, Response, Router } from "express";
+import { red, yellow } from "picocolors";
+import { LessThan, MoreThan } from "typeorm";
 
 let bcrypt: any;
 try {
@@ -14,17 +30,28 @@ const router: Router = Router();
 
 router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => {
 	const body = req.body as RegisterSchema;
-	const { register, security } = Config.get();
+	const { register, security, limits } = 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 referrer starts with any valid registration token
+	//!! bypasses captcha and registration disabling !!//
+	let validToken = false;
+	if (req.get("Referrer") && req.get("Referrer")?.includes("token=")) {
+		let token = req.get("Referrer")?.split("token=")[1].split("&")[0];
+		if (token) {
+			await ValidRegistrationToken.delete({ expires_at: LessThan(new Date()) });
+			let registrationToken = await ValidRegistrationToken.findOne({ where: { token: token, expires_at: MoreThan(new Date()) } });
+			if (registrationToken) {
+				console.log(yellow(`[REGISTER] Registration token ${token} used for registration!`));
+				await ValidRegistrationToken.delete(token);
+				validToken = true;
+			} else {
+				console.log(yellow(`[REGISTER] Invalid registration token ${token} used for registration by ${ip}!`));
+			}
+		}
 	}
 
 	// check if the user agreed to the Terms of Service
@@ -34,22 +61,7 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 		});
 	}
 
-	if (register.disabled) {
-		throw FieldErrors({
-			email: {
-				code: "DISABLED",
-				message: "registration is disabled on this instance"
-			}
-		});
-	}
-
-	if (!register.allowGuests) {
-		throw FieldErrors({
-			email: { code: "GUESTS_DISABLED", message: req.t("auth:register.GUESTS_DISABLED") }
-		});
-	}
-
-	if (register.requireCaptcha && security.captcha.enabled) {
+	if (register.requireCaptcha && security.captcha.enabled && !validToken) {
 		const { sitekey, service } = security.captcha;
 		if (!body.captcha_key) {
 			return res?.status(400).json({
@@ -69,24 +81,24 @@ 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: body.fingerprint }, select: ["id"] });
-
-		if (exists) {
-			throw FieldErrors({
-				email: {
-					code: "EMAIL_ALREADY_REGISTERED",
-					message: req.t("auth:register.EMAIL_ALREADY_REGISTERED")
-				}
-			});
-		}
+	// check if registration is allowed
+	if (!register.allowNewRegistration && !validToken) {
+		throw FieldErrors({
+			email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") }
+		});
 	}
 
 	if (register.blockProxies) {
-		if (isProxy(await IPAnalysis(ip))) {
-			console.log(`proxy ${ip} blocked from registration`);
-			throw new HTTPError("Your IP is blocked from registration");
+		let data;
+		try {
+			data = await IPAnalysis(ip);
+		} catch (e: any) {
+			console.warn(red(`[REGISTER]: Failed to analyze IP ${ip}: failed to contact api.ipdata.co!`), e.message);
+		}
+
+		if (data && isProxy(data)) {
+			console.log(yellow(`[REGISTER] Proxy ${ip} blocked from registration!`));
+			throw new HTTPError(req.t("auth:register.IP_BLOCKED"));
 		}
 	}
 
@@ -94,15 +106,10 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 	// TODO: check password strength
 
 	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") } });
-		}
-
 		// check if there is already an account with this email
 		const exists = await User.findOne({ where: { email: email } });
 
-		if (exists) {
+		if (exists && !register.disabled) {
 			throw FieldErrors({
 				email: {
 					code: "EMAIL_ALREADY_REGISTERED",
@@ -153,6 +160,46 @@ router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Re
 		});
 	}
 
+	if (
+		!validToken &&
+		limits.absoluteRate.register.enabled &&
+		(await await User.count({ where: { created_at: MoreThan(new Date(Date.now() - limits.absoluteRate.register.window)) } })) >=
+			limits.absoluteRate.register.limit
+	) {
+		console.log(
+			yellow(
+				`[REGISTER] Global register rate limit exceeded for ${getIpAdress(req)}: ${
+					process.env.LOG_SENSITIVE ? req.body.email : "<email redacted>"
+				}, ${req.body.username}, ${req.body.invite ?? "No invite given"}`
+			)
+		);
+		let oldest = await User.findOne({
+			where: { created_at: MoreThan(new Date(Date.now() - limits.absoluteRate.register.window)) },
+			order: { created_at: "ASC" }
+		});
+		if (!oldest) {
+			console.warn(
+				red(
+					`[REGISTER/WARN] Global rate limits exceeded, but no oldest user found. This should not happen. Did you misconfigure the limits?`
+				)
+			);
+		} else {
+			let retryAfterSec = Math.ceil(
+				(oldest!.created_at.getTime() - new Date(Date.now() - limits.absoluteRate.register.window).getTime()) / 1000
+			);
+			return res
+				.status(429)
+				.set("X-RateLimit-Limit", `${limits.absoluteRate.register.limit}`)
+				.set("X-RateLimit-Remaining", "0")
+				.set("X-RateLimit-Reset", `${(oldest!.created_at.getTime() + limits.absoluteRate.register.window) / 1000}`)
+				.set("X-RateLimit-Reset-After", `${retryAfterSec}`)
+				.set("X-RateLimit-Global", `true`)
+				.set("Retry-After", `${retryAfterSec}`)
+				.set("X-RateLimit-Bucket", `register`)
+				.send({ message: req.t("auth:register.TOO_MANY_REGISTRATIONS"), retry_after: retryAfterSec, global: true });
+		}
+	}
+
 	const user = await User.register({ ...body, req });
 
 	if (body.invite) {
diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts
index 3a1d2666..5fb8f5df 100644
--- a/src/api/routes/channels/#channel_id/invites.ts
+++ b/src/api/routes/channels/#channel_id/invites.ts
@@ -15,7 +15,7 @@ router.post(
 		isTextChannel(channel.type);
 
 		if (!channel.guild_id) {
-			throw new HTTPError("This channel doesn't exist", 404);
+			throw new HTTPError(req.t("common:notfound.CHANNEL"), 404);
 		}
 		const { guild_id } = channel;
 
@@ -46,7 +46,7 @@ router.get("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, r
 	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
 
 	if (!channel.guild_id) {
-		throw new HTTPError("This channel doesn't exist", 404);
+		throw new HTTPError(req.t("common:notfound.CHANNEL"), 404);
 	}
 	const { guild_id } = channel;
 
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
index 44de5c45..f13f5f94 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
@@ -63,7 +63,7 @@ router.delete("/:emoji", route({ permission: "MANAGE_MESSAGES" }), async (req: R
 	const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } });
 
 	const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
-	if (!already_added) throw new HTTPError("Reaction not found", 404);
+	if (!already_added) throw new HTTPError(req.t("common:notfound.REACTION"), 404);
 	message.reactions.remove(already_added);
 
 	await Promise.all([
@@ -89,7 +89,7 @@ router.get("/:emoji", route({ permission: "VIEW_CHANNEL" }), async (req: Request
 
 	const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } });
 	const reaction = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
-	if (!reaction) throw new HTTPError("Reaction not found", 404);
+	if (!reaction) throw new HTTPError(req.t("common:notfound.REACTION"), 404);
 
 	const users = await User.find({
 		where: {
@@ -163,7 +163,7 @@ router.delete("/:emoji/:user_id", route({}), async (req: Request, res: Response)
 	}
 
 	const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
-	if (!already_added || !already_added.user_ids.includes(user_id)) throw new HTTPError("Reaction not found", 404);
+	if (!already_added || !already_added.user_ids.includes(user_id)) throw new HTTPError(req.t("common:notfound.REACTION"), 404);
 
 	already_added.count--;
 
diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts
index 5fdcb6f9..87c689ec 100644
--- a/src/api/routes/channels/#channel_id/messages/index.ts
+++ b/src/api/routes/channels/#channel_id/messages/index.ts
@@ -6,17 +6,21 @@ import {
 	Config,
 	DmChannelDTO,
 	emitEvent,
+	getIpAdress,
 	getPermission,
+	getRights,
 	HTTPError,
 	Member,
 	Message,
 	MessageCreateEvent,
 	MessageCreateSchema,
+	Rights,
 	Snowflake,
 	uploadFile
 } from "@fosscord/util";
 import { Request, Response, Router } from "express";
 import multer from "multer";
+import { red, yellow } from "picocolors";
 import { FindManyOptions, LessThan, MoreThan } from "typeorm";
 import { URL } from "url";
 
@@ -53,7 +57,7 @@ export function isTextChannel(type: ChannelType): boolean {
 router.get("/", async (req: Request, res: Response) => {
 	const channel_id = req.params.channel_id;
 	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
-	if (!channel) throw new HTTPError("Channel not found", 404);
+	if (!channel) throw new HTTPError(req.t("common:notfound.CHANNEL"), 404);
 
 	isTextChannel(channel.type);
 	const around = req.query.around ? `${req.query.around}` : undefined;
@@ -159,6 +163,46 @@ router.post(
 		if (!channel.isWritable()) {
 			throw new HTTPError(`Cannot send messages to channel of type ${channel.type}`, 400);
 		}
+		var limits = Config.get().limits;
+
+		if (
+			!(await getRights(req.user_id)).has(Rights.FLAGS.BYPASS_RATE_LIMITS) &&
+			limits.absoluteRate.sendMessage.enabled &&
+			(await Message.count({
+				where: { channel_id, timestamp: MoreThan(new Date(Date.now() - limits.absoluteRate.sendMessage.window)) }
+			})) >= limits.absoluteRate.sendMessage.limit
+		) {
+			console.log(
+				yellow(
+					`[MESSAGE] Global register rate limit exceeded for ${getIpAdress(req)}: ${channel_id}, ${req.user_id}, ${body.content}`
+				)
+			);
+			let oldest = await Message.findOne({
+				where: { channel_id, timestamp: MoreThan(new Date(Date.now() - limits.absoluteRate.sendMessage.window)) },
+				order: { timestamp: "ASC" }
+			});
+			if (!oldest) {
+				console.warn(
+					red(
+						`[MESSAGE/WARN] Global rate limits exceeded, but no oldest message found. This should not happen. Did you misconfigure the limits?`
+					)
+				);
+			} else {
+				let retryAfterSec = Math.ceil(
+					(oldest!.timestamp.getTime() - new Date(Date.now() - limits.absoluteRate.sendMessage.window).getTime()) / 1000
+				);
+				return res
+					.status(429)
+					.set("X-RateLimit-Limit", `${limits.absoluteRate.sendMessage.limit}`)
+					.set("X-RateLimit-Remaining", "0")
+					.set("X-RateLimit-Reset", `${(oldest!.timestamp.getTime() + limits.absoluteRate.sendMessage.window) / 1000}`)
+					.set("X-RateLimit-Reset-After", `${retryAfterSec}`)
+					.set("X-RateLimit-Global", `false`)
+					.set("Retry-After", `${retryAfterSec}`)
+					.set("X-RateLimit-Bucket", `chnl_${channel_id}`)
+					.send({ message: req.t("common:toomany.MESSAGE"), retry_after: retryAfterSec, global: false });
+			}
+		}
 
 		const files = (req.files as Express.Multer.File[]) ?? [];
 		for (let currFile of files) {
diff --git a/src/api/routes/channels/#channel_id/permissions.ts b/src/api/routes/channels/#channel_id/permissions.ts
index bd462ea6..6b888c80 100644
--- a/src/api/routes/channels/#channel_id/permissions.ts
+++ b/src/api/routes/channels/#channel_id/permissions.ts
@@ -21,12 +21,12 @@ router.put(
 		const body = req.body as ChannelPermissionOverwriteSchema;
 
 		let channel = await Channel.findOneOrFail({ where: { id: channel_id } });
-		if (!channel.guild_id) throw new HTTPError("Channel not found", 404);
+		if (!channel.guild_id) throw new HTTPError(req.t("common:notfound.CHANNEL"), 404);
 
 		if (body.type === 0) {
-			if (!(await Role.count({ where: { id: overwrite_id } }))) throw new HTTPError("role not found", 404);
+			if (!(await Role.count({ where: { id: overwrite_id } }))) throw new HTTPError(req.t("common:notfound.ROLE"), 404);
 		} else if (body.type === 1) {
-			if (!(await Member.count({ where: { id: overwrite_id } }))) throw new HTTPError("user not found", 404);
+			if (!(await Member.count({ where: { id: overwrite_id } }))) throw new HTTPError(req.t("common:notfound.USER"), 404);
 		} else throw new HTTPError("type not supported", 501);
 
 		// @ts-ignore
@@ -60,7 +60,7 @@ router.delete("/:overwrite_id", route({ permission: "MANAGE_ROLES" }), async (re
 	const { channel_id, overwrite_id } = req.params;
 
 	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
-	if (!channel.guild_id) throw new HTTPError("Channel not found", 404);
+	if (!channel.guild_id) throw new HTTPError(req.t("common:notfound.CHANNEL"), 404);
 
 	channel.permission_overwrites = channel.permission_overwrites!.filter((x) => x.id === overwrite_id);
 
diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts
index 38dcb869..14e14c8b 100644
--- a/src/api/routes/channels/#channel_id/webhooks.ts
+++ b/src/api/routes/channels/#channel_id/webhooks.ts
@@ -23,7 +23,7 @@ router.post("/", route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOO
 
 	let { avatar, name } = req.body as { name: string; avatar?: string };
 	name = trimSpecial(name);
-	if (name === "clyde") throw new HTTPError("Invalid name", 400);
+	if (name.toLowerCase() === "clyde") throw new HTTPError("Invalid name", 400);
 
 	// TODO: save webhook in database and send response
 	res.json(new Webhook());
diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts
index 4600b4cb..4963d36a 100644
--- a/src/api/routes/guilds/#guild_id/bans.ts
+++ b/src/api/routes/guilds/#guild_id/bans.ts
@@ -1,10 +1,11 @@
-import { getIpAdress, route } from "@fosscord/api";
+import { route } from "@fosscord/api";
 import {
 	Ban,
 	BanModeratorSchema,
 	BanRegistrySchema,
 	DiscordApiErrors,
 	emitEvent,
+	getIpAdress,
 	GuildBanAddEvent,
 	GuildBanRemoveEvent,
 	HTTPError,
diff --git a/src/api/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts
index aa57ec65..4a5f5eca 100644
--- a/src/api/routes/guilds/#guild_id/regions.ts
+++ b/src/api/routes/guilds/#guild_id/regions.ts
@@ -1,5 +1,5 @@
-import { getIpAdress, getVoiceRegions, route } from "@fosscord/api";
-import { Guild } from "@fosscord/util";
+import { getVoiceRegions, route } from "@fosscord/api";
+import { getIpAdress, Guild } from "@fosscord/util";
 import { Request, Response, Router } from "express";
 
 const router = Router();
diff --git a/src/api/routes/guilds/#guild_id/stickers.ts b/src/api/routes/guilds/#guild_id/stickers.ts
index 15741780..06dade89 100644
--- a/src/api/routes/guilds/#guild_id/stickers.ts
+++ b/src/api/routes/guilds/#guild_id/stickers.ts
@@ -37,7 +37,7 @@ router.post(
 	bodyParser,
 	route({ permission: "MANAGE_EMOJIS_AND_STICKERS", body: "ModifyGuildStickerSchema" }),
 	async (req: Request, res: Response) => {
-		if (!req.file) throw new HTTPError("missing file");
+		if (!req.file) throw new HTTPError(req.t("common:body.MISSING_FILE"));
 
 		const { guild_id } = req.params;
 		const body = req.body as ModifyGuildStickerSchema;
diff --git a/src/api/routes/guilds/#guild_id/templates.ts b/src/api/routes/guilds/#guild_id/templates.ts
index 448ee033..af116760 100644
--- a/src/api/routes/guilds/#guild_id/templates.ts
+++ b/src/api/routes/guilds/#guild_id/templates.ts
@@ -1,5 +1,5 @@
-import { generateCode, route } from "@fosscord/api";
-import { Guild, HTTPError, OrmUtils, Template } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { generateCode, Guild, HTTPError, OrmUtils, Template } from "@fosscord/util";
 import { Request, Response, Router } from "express";
 
 const router: Router = Router();
diff --git a/src/api/routes/guilds/#guild_id/widget.json.ts b/src/api/routes/guilds/#guild_id/widget.json.ts
index 368fe46e..66cc456f 100644
--- a/src/api/routes/guilds/#guild_id/widget.json.ts
+++ b/src/api/routes/guilds/#guild_id/widget.json.ts
@@ -1,5 +1,5 @@
-import { random, route } from "@fosscord/api";
-import { Channel, Guild, HTTPError, Invite, Member, OrmUtils, Permissions } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { Channel, Guild, HTTPError, Invite, Member, OrmUtils, Permissions, random } from "@fosscord/util";
 import { Request, Response, Router } from "express";
 
 const router: Router = Router();
diff --git a/src/api/routes/users/@me/relationships.ts b/src/api/routes/users/@me/relationships.ts
index 8267c142..6383f3f3 100644
--- a/src/api/routes/users/@me/relationships.ts
+++ b/src/api/routes/users/@me/relationships.ts
@@ -69,7 +69,7 @@ router.post("/", route({ body: "RelationshipPostSchema" }), async (req: Request,
 
 router.delete("/:id", route({}), async (req: Request, res: Response) => {
 	const { id } = req.params;
-	if (id === req.user_id) throw new HTTPError("You can't remove yourself as a friend");
+	if (id === req.user_id) throw new HTTPError(req.t("common:relationship.REMOVE_SELF"));
 
 	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: userProjection, relations: ["relationships"] });
 	const friend = await User.findOneOrFail({ where: { id: id }, select: userProjection, relations: ["relationships"] });
@@ -77,7 +77,7 @@ router.delete("/:id", route({}), async (req: Request, res: Response) => {
 	const relationship = user.relationships.find((x) => x.to_id === id);
 	const friendRequest = friend.relationships.find((x) => x.to_id === req.user_id);
 
-	if (!relationship) throw new HTTPError("You are not friends with the user", 404);
+	if (!relationship) throw new HTTPError(req.t("common:relationship.NOT_FRIENDS"));
 	if (relationship?.type === RelationshipType.blocked) {
 		// unblock user
 
@@ -118,7 +118,7 @@ export default router;
 
 async function updateRelationship(req: Request, res: Response, friend: User, type: RelationshipType) {
 	const id = friend.id;
-	if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend");
+	if (id === req.user_id) throw new HTTPError(req.t("common:relationship.ADD_SELF"));
 
 	const user = await User.findOneOrFail({
 		where: { id: req.user_id },
@@ -132,7 +132,7 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ
 	// TODO: you can add infinitely many blocked users (should this be prevented?)
 	if (type === RelationshipType.blocked) {
 		if (relationship) {
-			if (relationship.type === RelationshipType.blocked) throw new HTTPError("You already blocked the user");
+			if (relationship.type === RelationshipType.blocked) throw new HTTPError(req.t("common:relationship.ALREADY_BLOCKED"));
 			relationship.type = RelationshipType.blocked;
 			await relationship.save();
 		} else {
@@ -178,17 +178,18 @@ async function updateRelationship(req: Request, res: Response, friend: User, typ
 	});
 
 	if (friendRequest) {
-		if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you");
-		if (friendRequest.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user");
+		//TODO: shouldn't this be failed silently?
+		if (friendRequest.type === RelationshipType.blocked) throw new HTTPError(req.t("common:relationship.BLOCKED"));
+		if (friendRequest.type === RelationshipType.friends) throw new HTTPError(req.t("common:relationship.ALREADY_FRIENDS"));
 		// accept friend request
 		incoming_relationship = friendRequest as any; //TODO: checkme, any cast
 		incoming_relationship.type = RelationshipType.friends;
 	}
 
 	if (relationship) {
-		if (relationship.type === RelationshipType.outgoing) throw new HTTPError("You already sent a friend request");
-		if (relationship.type === RelationshipType.blocked) throw new HTTPError("Unblock the user before sending a friend request");
-		if (relationship.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user");
+		if (relationship.type === RelationshipType.outgoing) throw new HTTPError(req.t("common:relationship.ALREADY_SENT"));
+		if (relationship.type === RelationshipType.blocked) throw new HTTPError(req.t("common:relationship.UNBLOCK"));
+		if (relationship.type === RelationshipType.friends) throw new HTTPError(req.t("common:relationship.ALREADY_FRIENDS"));
 		outgoing_relationship = relationship as any; //TODO: checkme, any cast
 		outgoing_relationship.type = RelationshipType.friends;
 	}
diff --git a/src/api/routes/voice/regions.ts b/src/api/routes/voice/regions.ts
index eacdcf11..1b5541e7 100644
--- a/src/api/routes/voice/regions.ts
+++ b/src/api/routes/voice/regions.ts
@@ -1,4 +1,5 @@
-import { getIpAdress, getVoiceRegions, route } from "@fosscord/api";
+import { getVoiceRegions, route } from "@fosscord/api";
+import { getIpAdress } from "@fosscord/util";
 import { Request, Response, Router } from "express";
 
 const router: Router = Router();
diff --git a/src/api/util/handlers/Voice.ts b/src/api/util/handlers/Voice.ts
index 4d60eb91..98d28ff0 100644
--- a/src/api/util/handlers/Voice.ts
+++ b/src/api/util/handlers/Voice.ts
@@ -1,5 +1,4 @@
-import { Config } from "@fosscord/util";
-import { distanceBetweenLocations, IPAnalysis } from "../utility/ipAddress";
+import { Config, distanceBetweenLocations, IPAnalysis } from "@fosscord/util";
 
 export async function getVoiceRegions(ipAddress: string, vip: boolean) {
 	const regions = Config.get().regions;
diff --git a/src/api/util/index.ts b/src/api/util/index.ts
index d06860cd..46cbd5ba 100644
--- a/src/api/util/index.ts
+++ b/src/api/util/index.ts
@@ -2,9 +2,3 @@ export * from "./entities/AssetCacheItem";
 export * from "./handlers/Message";
 export * from "./handlers/route";
 export * from "./handlers/Voice";
-export * from "./utility/Base64";
-export * from "./utility/captcha";
-export * from "./utility/ipAddress";
-export * from "./utility/passwordStrength";
-export * from "./utility/RandomInviteID";
-export * from "./utility/String";
diff --git a/src/api/util/utility/String.ts b/src/api/util/utility/String.ts
deleted file mode 100644
index a2e491e4..00000000
--- a/src/api/util/utility/String.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { FieldErrors } from "@fosscord/util";
-import { Request } from "express";
-import { ntob } from "./Base64";
-
-export function checkLength(str: string, min: number, max: number, key: string, req: Request) {
-	if (str.length < min || str.length > max) {
-		throw FieldErrors({
-			[key]: {
-				code: "BASE_TYPE_BAD_LENGTH",
-				message: req.t("common:field.BASE_TYPE_BAD_LENGTH", { length: `${min} - ${max}` })
-			}
-		});
-	}
-}
-
-export function generateCode() {
-	return ntob(Date.now() + Math.randomIntBetween(0, 10000));
-}
diff --git a/src/cdn/routes/attachments.ts b/src/cdn/routes/attachments.ts
index 013f03d8..530d3a8b 100644
--- a/src/cdn/routes/attachments.ts
+++ b/src/cdn/routes/attachments.ts
@@ -10,8 +10,9 @@ const router = Router();
 const SANITIZED_CONTENT_TYPE = ["text/html", "text/mhtml", "multipart/related", "application/xhtml+xml"];
 
 router.post("/:channel_id", multer.single("file"), async (req: Request, res: Response) => {
-	if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
-	if (!req.file) throw new HTTPError("file missing");
+	if (req.headers.signature !== Config.get().security.requestSignature)
+		throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE"));
+	if (!req.file) throw new HTTPError(req.t("common:body.MISSING_FILE"));
 
 	const { buffer, mimetype, size, originalname, fieldname } = req.file;
 	const { channel_id } = req.params;
@@ -49,7 +50,7 @@ router.get("/:channel_id/:id/:filename", async (req: Request, res: Response) =>
 	const { channel_id, id, filename } = req.params;
 
 	const file = await storage.get(`attachments/${channel_id}/${id}/${filename}`);
-	if (!file) throw new HTTPError("File not found");
+	if (!file) throw new HTTPError(req.t("common:notfound.FILE"));
 	const type = await FileType.fromBuffer(file);
 	let content_type = type?.mime || "application/octet-stream";
 
@@ -64,7 +65,8 @@ router.get("/:channel_id/:id/:filename", async (req: Request, res: Response) =>
 });
 
 router.delete("/:channel_id/:id/:filename", async (req: Request, res: Response) => {
-	if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
+	if (req.headers.signature !== Config.get().security.requestSignature)
+		throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE"));
 
 	const { channel_id, id, filename } = req.params;
 	const path = `attachments/${channel_id}/${id}/${filename}`;
diff --git a/src/cdn/routes/avatars.ts b/src/cdn/routes/avatars.ts
index fa26938f..2fa88987 100644
--- a/src/cdn/routes/avatars.ts
+++ b/src/cdn/routes/avatars.ts
@@ -17,8 +17,9 @@ const ALLOWED_MIME_TYPES = [...ANIMATED_MIME_TYPES, ...STATIC_MIME_TYPES];
 const router = Router();
 
 router.post("/:user_id", multer.single("file"), async (req: Request, res: Response) => {
-	if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
-	if (!req.file) throw new HTTPError("Missing file");
+	if (req.headers.signature !== Config.get().security.requestSignature)
+		throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE"));
+	if (!req.file) throw new HTTPError(req.t("common:body.MISSING_FILE"));
 	const { buffer, mimetype, size, originalname, fieldname } = req.file;
 	const { user_id } = req.params;
 
@@ -47,7 +48,7 @@ router.get("/:user_id", async (req: Request, res: Response) => {
 	const path = `avatars/${user_id}`;
 
 	const file = await storage.get(path);
-	if (!file) throw new HTTPError("not found", 404);
+	if (!file) throw new HTTPError(req.t("common:notfound.FILE"), 404);
 	const type = await FileType.fromBuffer(file);
 
 	res.set("Content-Type", type?.mime);
@@ -62,7 +63,7 @@ router.get("/:user_id/:hash", async (req: Request, res: Response) => {
 	const path = `avatars/${user_id}/${hash}`;
 
 	const file = await storage.get(path);
-	if (!file) throw new HTTPError("not found", 404);
+	if (!file) throw new HTTPError(req.t("common:notfound.FILE"), 404);
 	const type = await FileType.fromBuffer(file);
 
 	res.set("Content-Type", type?.mime);
@@ -72,7 +73,8 @@ router.get("/:user_id/:hash", async (req: Request, res: Response) => {
 });
 
 router.delete("/:user_id/:id", async (req: Request, res: Response) => {
-	if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
+	if (req.headers.signature !== Config.get().security.requestSignature)
+		throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE"));
 	const { user_id, id } = req.params;
 	const path = `avatars/${user_id}/${id}`;
 
diff --git a/src/cdn/routes/external.ts b/src/cdn/routes/external.ts
index 7ccf9b8a..43c4e505 100644
--- a/src/cdn/routes/external.ts
+++ b/src/cdn/routes/external.ts
@@ -19,7 +19,8 @@ const DEFAULT_FETCH_OPTIONS: any = {
 };
 
 router.post("/", async (req: Request, res: Response) => {
-	if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
+	if (req.headers.signature !== Config.get().security.requestSignature)
+		throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE"));
 
 	if (!req.body) throw new HTTPError("Invalid Body");
 
@@ -44,7 +45,7 @@ router.get("/:id", async (req: Request, res: Response) => {
 	const { id } = req.params;
 
 	const file = await storage.get(`/external/${id}`);
-	if (!file) throw new HTTPError("File not found");
+	if (!file) throw new HTTPError(req.t("common:notfound.FILE"));
 	const result = await FileType.fromBuffer(file);
 
 	res.set("Content-Type", result?.mime);
diff --git a/src/cdn/routes/guild-profiles.ts b/src/cdn/routes/guild-profiles.ts
index 32c05ad9..4ae492ea 100644
--- a/src/cdn/routes/guild-profiles.ts
+++ b/src/cdn/routes/guild-profiles.ts
@@ -17,8 +17,9 @@ const ALLOWED_MIME_TYPES = [...ANIMATED_MIME_TYPES, ...STATIC_MIME_TYPES];
 const router = Router();
 
 router.post("/", multer.single("file"), async (req: Request, res: Response) => {
-	if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
-	if (!req.file) throw new HTTPError("Missing file");
+	if (req.headers.signature !== Config.get().security.requestSignature)
+		throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE"));
+	if (!req.file) throw new HTTPError(req.t("common:body.MISSING_FILE"));
 	const { buffer, mimetype, size, originalname, fieldname } = req.file;
 	const { guild_id, user_id } = req.params;
 
@@ -47,7 +48,7 @@ router.get("/", async (req: Request, res: Response) => {
 	const path = `guilds/${guild_id}/users/${user_id}/avatars`;
 
 	const file = await storage.get(path);
-	if (!file) throw new HTTPError("not found", 404);
+	if (!file) throw new HTTPError(req.t("common:notfound.FILE"), 404);
 	const type = await FileType.fromBuffer(file);
 
 	res.set("Content-Type", type?.mime);
@@ -62,7 +63,7 @@ router.get("/:hash", async (req: Request, res: Response) => {
 	const path = `guilds/${guild_id}/users/${user_id}/avatars/${hash}`;
 
 	const file = await storage.get(path);
-	if (!file) throw new HTTPError("not found", 404);
+	if (!file) throw new HTTPError(req.t("common:notfound.FILE"), 404);
 	const type = await FileType.fromBuffer(file);
 
 	res.set("Content-Type", type?.mime);
@@ -72,7 +73,8 @@ router.get("/:hash", async (req: Request, res: Response) => {
 });
 
 router.delete("/:id", async (req: Request, res: Response) => {
-	if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
+	if (req.headers.signature !== Config.get().security.requestSignature)
+		throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE"));
 	const { guild_id, user_id, id } = req.params;
 	const path = `guilds/${guild_id}/users/${user_id}/avatars/${id}`;
 
diff --git a/src/cdn/routes/role-icons.ts b/src/cdn/routes/role-icons.ts
index 768e194f..b0946eaa 100644
--- a/src/cdn/routes/role-icons.ts
+++ b/src/cdn/routes/role-icons.ts
@@ -17,8 +17,9 @@ const ALLOWED_MIME_TYPES = [...STATIC_MIME_TYPES];
 const router = Router();
 
 router.post("/:role_id", multer.single("file"), async (req: Request, res: Response) => {
-	if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
-	if (!req.file) throw new HTTPError("Missing file");
+	if (req.headers.signature !== Config.get().security.requestSignature)
+		throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE"));
+	if (!req.file) throw new HTTPError(req.t("common:body.MISSING_FILE"));
 	const { buffer, mimetype, size, originalname, fieldname } = req.file;
 	const { role_id } = req.params;
 
@@ -46,7 +47,7 @@ router.get("/:role_id", async (req: Request, res: Response) => {
 	const path = `role-icons/${role_id}`;
 
 	const file = await storage.get(path);
-	if (!file) throw new HTTPError("not found", 404);
+	if (!file) throw new HTTPError(req.t("common:notfound.FILE"), 404);
 	const type = await FileType.fromBuffer(file);
 
 	res.set("Content-Type", type?.mime);
@@ -61,7 +62,7 @@ router.get("/:role_id/:hash", async (req: Request, res: Response) => {
 	const path = `role-icons/${role_id}/${hash}`;
 
 	const file = await storage.get(path);
-	if (!file) throw new HTTPError("not found", 404);
+	if (!file) throw new HTTPError(req.t("common:notfound.FILE"), 404);
 	const type = await FileType.fromBuffer(file);
 
 	res.set("Content-Type", type?.mime);
@@ -71,7 +72,8 @@ router.get("/:role_id/:hash", async (req: Request, res: Response) => {
 });
 
 router.delete("/:role_id/:id", async (req: Request, res: Response) => {
-	if (req.headers.signature !== Config.get().security.requestSignature) throw new HTTPError("Invalid request signature");
+	if (req.headers.signature !== Config.get().security.requestSignature)
+		throw new HTTPError(req.t("common:body.INVALID_REQUEST_SIGNATURE"));
 	const { role_id, id } = req.params;
 	const path = `role-icons/${role_id}/${id}`;
 
diff --git a/src/util/config/types/GeneralConfiguration.ts b/src/util/config/types/GeneralConfiguration.ts
index 5cb8df89..6d030645 100644
--- a/src/util/config/types/GeneralConfiguration.ts
+++ b/src/util/config/types/GeneralConfiguration.ts
@@ -3,6 +3,7 @@ import { Snowflake } from "../../util";
 export class GeneralConfiguration {
 	instanceName: string = "Fosscord Instance";
 	instanceDescription: string | null = "This is a Fosscord instance made in the pre-release days";
+	publicUrl: string = "http://localhost:3001";
 	frontPage: string | null = null;
 	tosPage: string | null = null;
 	correspondenceEmail: string | null = "noreply@localhost.local";
diff --git a/src/util/config/types/LimitConfigurations.ts b/src/util/config/types/LimitConfigurations.ts
index a3a52cf5..105fd1d6 100644
--- a/src/util/config/types/LimitConfigurations.ts
+++ b/src/util/config/types/LimitConfigurations.ts
@@ -1,4 +1,4 @@
-import { ChannelLimits, GuildLimits, MessageLimits, RateLimits, UserLimits } from ".";
+import { ChannelLimits, GlobalRateLimits, GuildLimits, MessageLimits, RateLimits, UserLimits } from ".";
 
 export class LimitsConfiguration {
 	user: UserLimits = new UserLimits();
@@ -6,4 +6,5 @@ export class LimitsConfiguration {
 	message: MessageLimits = new MessageLimits();
 	channel: ChannelLimits = new ChannelLimits();
 	rate: RateLimits = new RateLimits();
+	absoluteRate: GlobalRateLimits = new GlobalRateLimits();
 }
diff --git a/src/util/config/types/RegisterConfiguration.ts b/src/util/config/types/RegisterConfiguration.ts
index 68946272..caeab123 100644
--- a/src/util/config/types/RegisterConfiguration.ts
+++ b/src/util/config/types/RegisterConfiguration.ts
@@ -12,7 +12,6 @@ export class RegisterConfiguration {
 	allowGuests: boolean = true;
 	guestsRequireInvite: boolean = true;
 	allowNewRegistration: boolean = true;
-	allowMultipleAccounts: boolean = true;
 	blockProxies: boolean = true;
 	incrementingDiscriminators: boolean = false; // random otherwise
 	defaultRights: string = "0";
diff --git a/src/util/config/types/SecurityConfiguration.ts b/src/util/config/types/SecurityConfiguration.ts
index 5a3d5aa6..229587c3 100644
--- a/src/util/config/types/SecurityConfiguration.ts
+++ b/src/util/config/types/SecurityConfiguration.ts
@@ -17,4 +17,5 @@ export class SecurityConfiguration {
 	mfaBackupCodeCount: number = 10;
 	mfaBackupCodeBytes: number = 4;
 	statsWorldReadable: boolean = true;
+	defaultRegistrationTokenExpiration: number = 1000 * 60 * 60 * 24 * 7; //1 week
 }
diff --git a/src/util/config/types/subconfigurations/limits/GlobalRateLimits.ts b/src/util/config/types/subconfigurations/limits/GlobalRateLimits.ts
new file mode 100644
index 00000000..00526fb4
--- /dev/null
+++ b/src/util/config/types/subconfigurations/limits/GlobalRateLimits.ts
@@ -0,0 +1,10 @@
+export class GlobalRateLimits {
+	register: GlobalRateLimit = { limit: 25, window: 60 * 60 * 1000, enabled: true };
+	sendMessage: GlobalRateLimit = { limit: 120, window: 60 * 1000, enabled: true };
+}
+
+export class GlobalRateLimit {
+	limit: number = 100;
+	window: number = 60 * 60 * 1000;
+	enabled: boolean = true;
+}
diff --git a/src/util/config/types/subconfigurations/limits/index.ts b/src/util/config/types/subconfigurations/limits/index.ts
index a4911542..d2d24ccb 100644
--- a/src/util/config/types/subconfigurations/limits/index.ts
+++ b/src/util/config/types/subconfigurations/limits/index.ts
@@ -1,4 +1,5 @@
 export * from "./ChannelLimits";
+export * from "./GlobalRateLimits";
 export * from "./GuildLimits";
 export * from "./MessageLimits";
 export * from "./RateLimits";
diff --git a/src/util/entities/Attachment.ts b/src/util/entities/Attachment.ts
index 8392f415..c0ea3dec 100644
--- a/src/util/entities/Attachment.ts
+++ b/src/util/entities/Attachment.ts
@@ -1,6 +1,6 @@
 import { BeforeRemove, Column, Entity, JoinColumn, ManyToOne, RelationId } from "typeorm";
 import { URL } from "url";
-import { deleteFile } from "../util/cdn";
+import { deleteFile } from "../util/CDN";
 import { BaseClass } from "./BaseClass";
 
 @Entity("attachments")
diff --git a/src/util/entities/Invite.ts b/src/util/entities/Invite.ts
index f6ba85d7..151fcc59 100644
--- a/src/util/entities/Invite.ts
+++ b/src/util/entities/Invite.ts
@@ -1,4 +1,4 @@
-import { random } from "@fosscord/api";
+import { random } from "@fosscord/util";
 import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, RelationId } from "typeorm";
 import { BaseClassWithoutId } from "./BaseClass";
 import { Channel } from "./Channel";
diff --git a/src/util/entities/ValidRegistrationTokens.ts b/src/util/entities/ValidRegistrationTokens.ts
new file mode 100644
index 00000000..5d0747b8
--- /dev/null
+++ b/src/util/entities/ValidRegistrationTokens.ts
@@ -0,0 +1,12 @@
+import { BaseEntity, Column, Entity, PrimaryColumn } from "typeorm";
+import { Config } from "..";
+
+@Entity("valid_registration_tokens")
+export class ValidRegistrationToken extends BaseEntity {
+	@PrimaryColumn()
+	token: string;
+	@Column()
+	created_at: Date = new Date();
+	@Column()
+	expires_at: Date = new Date(Date.now() + Config.get().security.defaultRegistrationTokenExpiration);
+}
diff --git a/src/util/entities/index.ts b/src/util/entities/index.ts
index 2b91c2ba..5342d951 100644
--- a/src/util/entities/index.ts
+++ b/src/util/entities/index.ts
@@ -29,5 +29,6 @@ export * from "./TeamMember";
 export * from "./Template";
 export * from "./User";
 export * from "./UserSettings";
+export * from "./ValidRegistrationTokens";
 export * from "./VoiceState";
 export * from "./Webhook";
diff --git a/src/util/migrations/mariadb/1663440589234-registration_tokens.ts b/src/util/migrations/mariadb/1663440589234-registration_tokens.ts
new file mode 100644
index 00000000..364f8668
--- /dev/null
+++ b/src/util/migrations/mariadb/1663440589234-registration_tokens.ts
@@ -0,0 +1,30 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class registrationTokens1663440589234 implements MigrationInterface {
+	name = "registrationTokens1663440589234";
+
+	public async up(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`
+            CREATE TABLE \`valid_registration_tokens\` (
+                \`id\` varchar(255) NOT NULL,
+                \`token\` varchar(255) NOT NULL,
+                \`created_at\` datetime NOT NULL,
+                \`expires_at\` datetime NOT NULL,
+                PRIMARY KEY (\`id\`)
+            ) ENGINE = InnoDB
+        `);
+		await queryRunner.query(`
+            ALTER TABLE \`users\` DROP COLUMN \`notes\`
+        `);
+	}
+
+	public async down(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`
+            ALTER TABLE \`users\`
+            ADD \`notes\` text NOT NULL
+        `);
+		await queryRunner.query(`
+            DROP TABLE \`valid_registration_tokens\`
+        `);
+	}
+}
diff --git a/src/util/migrations/mariadb/1663448562034-drop_id_for_registration_tokens.ts b/src/util/migrations/mariadb/1663448562034-drop_id_for_registration_tokens.ts
new file mode 100644
index 00000000..58743d07
--- /dev/null
+++ b/src/util/migrations/mariadb/1663448562034-drop_id_for_registration_tokens.ts
@@ -0,0 +1,32 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class dropIdForRegistrationTokens1663448562034 implements MigrationInterface {
+	name = "dropIdForRegistrationTokens1663448562034";
+
+	public async up(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`
+            ALTER TABLE \`valid_registration_tokens\` DROP PRIMARY KEY
+        `);
+		await queryRunner.query(`
+            ALTER TABLE \`valid_registration_tokens\` DROP COLUMN \`id\`
+        `);
+		await queryRunner.query(`
+            ALTER TABLE \`valid_registration_tokens\`
+            ADD PRIMARY KEY (\`token\`)
+        `);
+	}
+
+	public async down(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`
+            ALTER TABLE \`valid_registration_tokens\` DROP PRIMARY KEY
+        `);
+		await queryRunner.query(`
+            ALTER TABLE \`valid_registration_tokens\`
+            ADD \`id\` varchar(255) NOT NULL
+        `);
+		await queryRunner.query(`
+            ALTER TABLE \`valid_registration_tokens\`
+            ADD PRIMARY KEY (\`id\`)
+        `);
+	}
+}
diff --git a/src/util/migrations/postgres/1663440587650-registration_tokens.ts b/src/util/migrations/postgres/1663440587650-registration_tokens.ts
new file mode 100644
index 00000000..d5f602b8
--- /dev/null
+++ b/src/util/migrations/postgres/1663440587650-registration_tokens.ts
@@ -0,0 +1,32 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class registrationTokens1663440587650 implements MigrationInterface {
+	name = "registrationTokens1663440587650";
+
+	public async up(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`
+            CREATE TABLE "valid_registration_tokens" (
+                "id" character varying NOT NULL,
+                "token" character varying NOT NULL,
+                "created_at" TIMESTAMP NOT NULL,
+                "expires_at" TIMESTAMP NOT NULL,
+                CONSTRAINT "PK_aac42a46cd46369450217de1c8a" PRIMARY KEY ("id")
+            )
+        `);
+		await queryRunner.query(`
+            ALTER TABLE "members"
+            ALTER COLUMN "bio" DROP DEFAULT
+        `);
+	}
+
+	public async down(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`
+            ALTER TABLE "members"
+            ALTER COLUMN "bio"
+            SET DEFAULT ''
+        `);
+		await queryRunner.query(`
+            DROP TABLE "valid_registration_tokens"
+        `);
+	}
+}
diff --git a/src/util/migrations/postgres/1663448561249-drop_id_for_registration_tokens.ts b/src/util/migrations/postgres/1663448561249-drop_id_for_registration_tokens.ts
new file mode 100644
index 00000000..4dc8c6ba
--- /dev/null
+++ b/src/util/migrations/postgres/1663448561249-drop_id_for_registration_tokens.ts
@@ -0,0 +1,32 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class dropIdForRegistrationTokens1663448561249 implements MigrationInterface {
+	name = "dropIdForRegistrationTokens1663448561249";
+
+	public async up(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`
+            ALTER TABLE "valid_registration_tokens" DROP CONSTRAINT "PK_aac42a46cd46369450217de1c8a"
+        `);
+		await queryRunner.query(`
+            ALTER TABLE "valid_registration_tokens" DROP COLUMN "id"
+        `);
+		await queryRunner.query(`
+            ALTER TABLE "valid_registration_tokens"
+            ADD CONSTRAINT "PK_e0f5c8e3fcefe3134a092c50485" PRIMARY KEY ("token")
+        `);
+	}
+
+	public async down(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`
+            ALTER TABLE "valid_registration_tokens" DROP CONSTRAINT "PK_e0f5c8e3fcefe3134a092c50485"
+        `);
+		await queryRunner.query(`
+            ALTER TABLE "valid_registration_tokens"
+            ADD "id" character varying NOT NULL
+        `);
+		await queryRunner.query(`
+            ALTER TABLE "valid_registration_tokens"
+            ADD CONSTRAINT "PK_aac42a46cd46369450217de1c8a" PRIMARY KEY ("id")
+        `);
+	}
+}
diff --git a/src/util/migrations/sqlite/1663440585960-registration_tokens.ts b/src/util/migrations/sqlite/1663440585960-registration_tokens.ts
new file mode 100644
index 00000000..520977c7
--- /dev/null
+++ b/src/util/migrations/sqlite/1663440585960-registration_tokens.ts
@@ -0,0 +1,245 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class registrationTokens1663440585960 implements MigrationInterface {
+	name = "registrationTokens1663440585960";
+
+	public async up(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`
+            CREATE TABLE "valid_registration_tokens" (
+                "id" varchar PRIMARY KEY NOT NULL,
+                "token" varchar NOT NULL,
+                "created_at" datetime NOT NULL,
+                "expires_at" datetime NOT NULL
+            )
+        `);
+		await queryRunner.query(`
+            CREATE TABLE "temporary_users" (
+                "id" varchar PRIMARY KEY NOT NULL,
+                "username" varchar NOT NULL,
+                "discriminator" varchar NOT NULL,
+                "avatar" varchar,
+                "accent_color" integer,
+                "banner" varchar,
+                "phone" varchar,
+                "desktop" boolean NOT NULL,
+                "mobile" boolean NOT NULL,
+                "premium" boolean NOT NULL,
+                "premium_type" integer NOT NULL,
+                "bot" boolean NOT NULL,
+                "bio" varchar,
+                "system" boolean NOT NULL,
+                "nsfw_allowed" boolean NOT NULL,
+                "mfa_enabled" boolean,
+                "totp_secret" varchar,
+                "totp_last_ticket" varchar,
+                "created_at" datetime NOT NULL,
+                "premium_since" datetime,
+                "verified" boolean NOT NULL,
+                "disabled" boolean NOT NULL,
+                "deleted" boolean NOT NULL,
+                "email" varchar,
+                "flags" varchar NOT NULL,
+                "public_flags" integer NOT NULL,
+                "rights" bigint NOT NULL,
+                "data" text NOT NULL,
+                "fingerprints" text NOT NULL,
+                "extended_settings" text NOT NULL,
+                "settingsId" varchar,
+                CONSTRAINT "UQ_b1dd13b6ed980004a795ca184a6" UNIQUE ("settingsId"),
+                CONSTRAINT "FK_76ba283779c8441fd5ff819c8cf" FOREIGN KEY ("settingsId") REFERENCES "user_settings" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
+            )
+        `);
+		await queryRunner.query(`
+            INSERT INTO "temporary_users"(
+                    "id",
+                    "username",
+                    "discriminator",
+                    "avatar",
+                    "accent_color",
+                    "banner",
+                    "phone",
+                    "desktop",
+                    "mobile",
+                    "premium",
+                    "premium_type",
+                    "bot",
+                    "bio",
+                    "system",
+                    "nsfw_allowed",
+                    "mfa_enabled",
+                    "totp_secret",
+                    "totp_last_ticket",
+                    "created_at",
+                    "premium_since",
+                    "verified",
+                    "disabled",
+                    "deleted",
+                    "email",
+                    "flags",
+                    "public_flags",
+                    "rights",
+                    "data",
+                    "fingerprints",
+                    "extended_settings",
+                    "settingsId"
+                )
+            SELECT "id",
+                "username",
+                "discriminator",
+                "avatar",
+                "accent_color",
+                "banner",
+                "phone",
+                "desktop",
+                "mobile",
+                "premium",
+                "premium_type",
+                "bot",
+                "bio",
+                "system",
+                "nsfw_allowed",
+                "mfa_enabled",
+                "totp_secret",
+                "totp_last_ticket",
+                "created_at",
+                "premium_since",
+                "verified",
+                "disabled",
+                "deleted",
+                "email",
+                "flags",
+                "public_flags",
+                "rights",
+                "data",
+                "fingerprints",
+                "extended_settings",
+                "settingsId"
+            FROM "users"
+        `);
+		await queryRunner.query(`
+            DROP TABLE "users"
+        `);
+		await queryRunner.query(`
+            ALTER TABLE "temporary_users"
+                RENAME TO "users"
+        `);
+	}
+
+	public async down(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`
+            ALTER TABLE "users"
+                RENAME TO "temporary_users"
+        `);
+		await queryRunner.query(`
+            CREATE TABLE "users" (
+                "id" varchar PRIMARY KEY NOT NULL,
+                "username" varchar NOT NULL,
+                "discriminator" varchar NOT NULL,
+                "avatar" varchar,
+                "accent_color" integer,
+                "banner" varchar,
+                "phone" varchar,
+                "desktop" boolean NOT NULL,
+                "mobile" boolean NOT NULL,
+                "premium" boolean NOT NULL,
+                "premium_type" integer NOT NULL,
+                "bot" boolean NOT NULL,
+                "bio" varchar,
+                "system" boolean NOT NULL,
+                "nsfw_allowed" boolean NOT NULL,
+                "mfa_enabled" boolean,
+                "totp_secret" varchar,
+                "totp_last_ticket" varchar,
+                "created_at" datetime NOT NULL,
+                "premium_since" datetime,
+                "verified" boolean NOT NULL,
+                "disabled" boolean NOT NULL,
+                "deleted" boolean NOT NULL,
+                "email" varchar,
+                "flags" varchar NOT NULL,
+                "public_flags" integer NOT NULL,
+                "rights" bigint NOT NULL,
+                "data" text NOT NULL,
+                "fingerprints" text NOT NULL,
+                "extended_settings" text NOT NULL,
+                "notes" text NOT NULL,
+                "settingsId" varchar,
+                CONSTRAINT "UQ_b1dd13b6ed980004a795ca184a6" UNIQUE ("settingsId"),
+                CONSTRAINT "FK_76ba283779c8441fd5ff819c8cf" FOREIGN KEY ("settingsId") REFERENCES "user_settings" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
+            )
+        `);
+		await queryRunner.query(`
+            INSERT INTO "users"(
+                    "id",
+                    "username",
+                    "discriminator",
+                    "avatar",
+                    "accent_color",
+                    "banner",
+                    "phone",
+                    "desktop",
+                    "mobile",
+                    "premium",
+                    "premium_type",
+                    "bot",
+                    "bio",
+                    "system",
+                    "nsfw_allowed",
+                    "mfa_enabled",
+                    "totp_secret",
+                    "totp_last_ticket",
+                    "created_at",
+                    "premium_since",
+                    "verified",
+                    "disabled",
+                    "deleted",
+                    "email",
+                    "flags",
+                    "public_flags",
+                    "rights",
+                    "data",
+                    "fingerprints",
+                    "extended_settings",
+                    "settingsId"
+                )
+            SELECT "id",
+                "username",
+                "discriminator",
+                "avatar",
+                "accent_color",
+                "banner",
+                "phone",
+                "desktop",
+                "mobile",
+                "premium",
+                "premium_type",
+                "bot",
+                "bio",
+                "system",
+                "nsfw_allowed",
+                "mfa_enabled",
+                "totp_secret",
+                "totp_last_ticket",
+                "created_at",
+                "premium_since",
+                "verified",
+                "disabled",
+                "deleted",
+                "email",
+                "flags",
+                "public_flags",
+                "rights",
+                "data",
+                "fingerprints",
+                "extended_settings",
+                "settingsId"
+            FROM "temporary_users"
+        `);
+		await queryRunner.query(`
+            DROP TABLE "temporary_users"
+        `);
+		await queryRunner.query(`
+            DROP TABLE "valid_registration_tokens"
+        `);
+	}
+}
diff --git a/src/util/migrations/sqlite/1663448560501-drop_id_for_registration_tokens.ts b/src/util/migrations/sqlite/1663448560501-drop_id_for_registration_tokens.ts
new file mode 100644
index 00000000..f99f2348
--- /dev/null
+++ b/src/util/migrations/sqlite/1663448560501-drop_id_for_registration_tokens.ts
@@ -0,0 +1,96 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class dropIdForRegistrationTokens1663448560501 implements MigrationInterface {
+	name = "dropIdForRegistrationTokens1663448560501";
+
+	public async up(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`
+            CREATE TABLE "temporary_valid_registration_tokens" (
+                "token" varchar NOT NULL,
+                "created_at" datetime NOT NULL,
+                "expires_at" datetime NOT NULL
+            )
+        `);
+		await queryRunner.query(`
+            INSERT INTO "temporary_valid_registration_tokens"("token", "created_at", "expires_at")
+            SELECT "token",
+                "created_at",
+                "expires_at"
+            FROM "valid_registration_tokens"
+        `);
+		await queryRunner.query(`
+            DROP TABLE "valid_registration_tokens"
+        `);
+		await queryRunner.query(`
+            ALTER TABLE "temporary_valid_registration_tokens"
+                RENAME TO "valid_registration_tokens"
+        `);
+		await queryRunner.query(`
+            CREATE TABLE "temporary_valid_registration_tokens" (
+                "token" varchar PRIMARY KEY NOT NULL,
+                "created_at" datetime NOT NULL,
+                "expires_at" datetime NOT NULL
+            )
+        `);
+		await queryRunner.query(`
+            INSERT INTO "temporary_valid_registration_tokens"("token", "created_at", "expires_at")
+            SELECT "token",
+                "created_at",
+                "expires_at"
+            FROM "valid_registration_tokens"
+        `);
+		await queryRunner.query(`
+            DROP TABLE "valid_registration_tokens"
+        `);
+		await queryRunner.query(`
+            ALTER TABLE "temporary_valid_registration_tokens"
+                RENAME TO "valid_registration_tokens"
+        `);
+	}
+
+	public async down(queryRunner: QueryRunner): Promise<void> {
+		await queryRunner.query(`
+            ALTER TABLE "valid_registration_tokens"
+                RENAME TO "temporary_valid_registration_tokens"
+        `);
+		await queryRunner.query(`
+            CREATE TABLE "valid_registration_tokens" (
+                "token" varchar NOT NULL,
+                "created_at" datetime NOT NULL,
+                "expires_at" datetime NOT NULL
+            )
+        `);
+		await queryRunner.query(`
+            INSERT INTO "valid_registration_tokens"("token", "created_at", "expires_at")
+            SELECT "token",
+                "created_at",
+                "expires_at"
+            FROM "temporary_valid_registration_tokens"
+        `);
+		await queryRunner.query(`
+            DROP TABLE "temporary_valid_registration_tokens"
+        `);
+		await queryRunner.query(`
+            ALTER TABLE "valid_registration_tokens"
+                RENAME TO "temporary_valid_registration_tokens"
+        `);
+		await queryRunner.query(`
+            CREATE TABLE "valid_registration_tokens" (
+                "id" varchar PRIMARY KEY NOT NULL,
+                "token" varchar NOT NULL,
+                "created_at" datetime NOT NULL,
+                "expires_at" datetime NOT NULL
+            )
+        `);
+		await queryRunner.query(`
+            INSERT INTO "valid_registration_tokens"("token", "created_at", "expires_at")
+            SELECT "token",
+                "created_at",
+                "expires_at"
+            FROM "temporary_valid_registration_tokens"
+        `);
+		await queryRunner.query(`
+            DROP TABLE "temporary_valid_registration_tokens"
+        `);
+	}
+}
diff --git a/src/api/util/utility/Base64.ts b/src/util/util/Base64.ts
index 46cff77a..46cff77a 100644
--- a/src/api/util/utility/Base64.ts
+++ b/src/util/util/Base64.ts
diff --git a/src/util/util/cdn.ts b/src/util/util/CDN.ts
index 5573b848..5573b848 100644
--- a/src/util/util/cdn.ts
+++ b/src/util/util/CDN.ts
diff --git a/src/api/util/utility/captcha.ts b/src/util/util/Captcha.ts
index 02983f3f..02983f3f 100644
--- a/src/api/util/utility/captcha.ts
+++ b/src/util/util/Captcha.ts
diff --git a/src/api/util/utility/ipAddress.ts b/src/util/util/IPAddress.ts
index c96feb9e..c96feb9e 100644
--- a/src/api/util/utility/ipAddress.ts
+++ b/src/util/util/IPAddress.ts
diff --git a/src/api/util/utility/passwordStrength.ts b/src/util/util/PasswordStrength.ts
index ff83d3df..ff83d3df 100644
--- a/src/api/util/utility/passwordStrength.ts
+++ b/src/util/util/PasswordStrength.ts
diff --git a/src/api/util/utility/RandomInviteID.ts b/src/util/util/RandomInviteID.ts
index feebfd3d..49302916 100644
--- a/src/api/util/utility/RandomInviteID.ts
+++ b/src/util/util/RandomInviteID.ts
@@ -1,13 +1,13 @@
 import { Snowflake } from "@fosscord/util";
+import crypto from "crypto";
 
-export function random(length = 6) {
+export function random(length = 6, chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") {
 	// Declare all characters
-	let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
 
 	// Pick characers randomly
 	let str = "";
 	for (let i = 0; i < length; i++) {
-		str += chars.charAt(Math.floor(Math.random() * chars.length));
+		str += chars.charAt(Math.floor(crypto.randomInt(chars.length)));
 	}
 
 	return str;
diff --git a/src/util/util/String.ts b/src/util/util/String.ts
index 55f11e8d..b14cd257 100644
--- a/src/util/util/String.ts
+++ b/src/util/util/String.ts
@@ -1,5 +1,23 @@
+import { FieldErrors } from "@fosscord/util";
+import { Request } from "express";
+import { ntob } from "./Base64";
 import { SPECIAL_CHAR } from "./Regex";
 
+export function checkLength(str: string, min: number, max: number, key: string, req: Request) {
+	if (str.length < min || str.length > max) {
+		throw FieldErrors({
+			[key]: {
+				code: "BASE_TYPE_BAD_LENGTH",
+				message: req.t("common:field.BASE_TYPE_BAD_LENGTH", { length: `${min} - ${max}` })
+			}
+		});
+	}
+}
+
+export function generateCode() {
+	return ntob(Date.now() + Math.randomIntBetween(0, 10000));
+}
+
 export function trimSpecial(str?: string): string {
 	// @ts-ignore
 	if (!str) return;
diff --git a/src/util/util/index.ts b/src/util/util/index.ts
index 11f0b72a..e10dc563 100644
--- a/src/util/util/index.ts
+++ b/src/util/util/index.ts
@@ -1,8 +1,9 @@
 export * from "./ApiError";
 export * from "./Array";
 export * from "./BitField";
+export * from "./Captcha";
 //export * from "./Categories";
-export * from "./cdn";
+export * from "./CDN";
 export * from "./Config";
 export * from "./Constants";
 export * from "./Database";
@@ -14,9 +15,11 @@ export * from "./imports/index";
 export * from "./imports/OrmUtils";
 export * from "./Intents";
 export * from "./InvisibleCharacters";
+export * from "./IPAddress";
 export * from "./MessageFlags";
 export * from "./Permissions";
 export * from "./RabbitMQ";
+export * from "./RandomInviteID";
 export * from "./Regex";
 export * from "./Rights";
 export * from "./Snowflake";