diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts
index de1cbd3d..33fc6062 100644
--- a/src/api/routes/auth/register.ts
+++ b/src/api/routes/auth/register.ts
@@ -50,9 +50,18 @@ router.post(
}),
async (req: Request, res: Response) => {
const body = req.body as RegisterSchema;
- const { register, security, limits } = Config.get();
+ const { register, security, limits, general } = Config.get();
const ip = getIpAdress(req);
+ if (!general.uniqueUsernames && body.unique_username_registration) {
+ throw FieldErrors({
+ unique_username_registration: {
+ code: "UNIQUE_USERNAMES_DISABLED",
+ message: req.t("auth:register.UNIQUE_USERNAMES_DISABLED"),
+ },
+ });
+ }
+
// Reg tokens
// They're a one time use token that bypasses registration limits ( rates, disabled reg, etc )
let regTokenUsed = false;
diff --git a/src/api/routes/unique-username/username-attempt-unauthed.ts b/src/api/routes/unique-username/username-attempt-unauthed.ts
new file mode 100644
index 00000000..a1f63a69
--- /dev/null
+++ b/src/api/routes/unique-username/username-attempt-unauthed.ts
@@ -0,0 +1,33 @@
+import { route } from "@spacebar/api";
+import { Config, User, UsernameAttemptUnauthedSchema } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+const router = Router();
+
+router.post(
+ "/",
+ route({
+ requestBody: "UsernameAttemptUnauthedSchema",
+ responses: {
+ 200: { body: "UsernameAttemptResponse" },
+ 400: { body: "APIErrorResponse" },
+ },
+ description: "Check if a username is available",
+ }),
+ async (req: Request, res: Response) => {
+ const body = req.body as UsernameAttemptUnauthedSchema;
+ const { uniqueUsernames } = Config.get().general;
+ if (!uniqueUsernames) {
+ throw new HTTPError(
+ "Unique Usernames feature is not enabled on this instance.",
+ 400,
+ );
+ }
+
+ res.json({
+ taken: !User.isUsernameAvailable(body.username),
+ });
+ },
+);
+
+export default router;
diff --git a/src/api/routes/unique-username/username-suggestions-unauthed.ts b/src/api/routes/unique-username/username-suggestions-unauthed.ts
new file mode 100644
index 00000000..9b112b55
--- /dev/null
+++ b/src/api/routes/unique-username/username-suggestions-unauthed.ts
@@ -0,0 +1,37 @@
+import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+import { Config } from "../../../util";
+const router = Router();
+
+router.get(
+ "/",
+ route({
+ query: {
+ global_name: {
+ type: "string",
+ required: false,
+ },
+ },
+ responses: {
+ 400: { body: "APIErrorResponse" },
+ },
+ }),
+ async (req: Request, res: Response) => {
+ const globalName = req.query.globalName as string | undefined;
+ const { uniqueUsernames } = Config.get().general;
+ if (!uniqueUsernames) {
+ throw new HTTPError(
+ "Unique Usernames feature is not enabled on this instance.",
+ 400,
+ );
+ }
+
+ // return a random suggestion
+ if (!globalName) return res.json({ username: "" });
+ // return a suggestion based on the globalName
+ return res.json({ username: globalName });
+ },
+);
+
+export default router;
diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts
index f4578126..55d2ce12 100644
--- a/src/api/routes/users/@me/index.ts
+++ b/src/api/routes/users/@me/index.ts
@@ -172,10 +172,7 @@ router.patch(
}
// check if username is already taken (pomelo only)
- const userCount = await User.count({
- where: { username: body.username },
- });
- if (userCount > 0) {
+ if (!User.isUsernameAvailable(body.username))
throw FieldErrors({
username: {
code: "USERNAME_ALREADY_TAKEN",
@@ -184,7 +181,6 @@ router.patch(
),
},
});
- }
}
// handle username changes (old username system)
diff --git a/src/util/entities/User.ts b/src/util/entities/User.ts
index d1fbb5c2..5ec9862e 100644
--- a/src/util/entities/User.ts
+++ b/src/util/entities/User.ts
@@ -344,6 +344,7 @@ export class User extends BaseClass {
password,
id,
req,
+ global_name,
}: {
username: string;
password?: string;
@@ -351,8 +352,10 @@ export class User extends BaseClass {
date_of_birth?: Date; // "2000-04-03"
id?: string;
req?: Request;
+ global_name?: string;
}) {
const { uniqueUsernames } = Config.get().general;
+ const { minUsername, maxUsername } = Config.get().limits.user;
// trim special uf8 control characters -> Backspace, Newline, ...
username = trimSpecial(username);
@@ -374,6 +377,34 @@ export class User extends BaseClass {
}
}
+ if (uniqueUsernames) {
+ // check if there is already an account with this username
+ if (!User.isUsernameAvailable(username))
+ throw FieldErrors({
+ username: {
+ code: "USERNAME_ALREADY_TAKEN",
+ message:
+ req?.t("common:field.USERNAME_ALREADY_TAKEN") || "",
+ },
+ });
+
+ // validate username length
+ if (
+ username.length < minUsername ||
+ username.length > maxUsername
+ ) {
+ throw FieldErrors({
+ username: {
+ code: "BASE_TYPE_BAD_LENGTH",
+ message:
+ req?.t("common:field.BASE_TYPE_BAD_LENGTH", {
+ length: `${minUsername} and ${maxUsername}`,
+ }) || "",
+ },
+ });
+ }
+ }
+
// 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
@@ -386,6 +417,7 @@ export class User extends BaseClass {
const user = User.create({
username: uniqueUsernames ? username.toLowerCase() : username,
+ global_name: uniqueUsernames ? global_name : undefined,
discriminator,
id: id || Snowflake.generate(),
email: email,
@@ -429,6 +461,14 @@ export class User extends BaseClass {
return user;
}
+
+ static async isUsernameAvailable(username: string) {
+ const user = await User.findOne({
+ where: { username },
+ select: ["id"],
+ });
+ return !user;
+ }
}
export const CUSTOM_USER_FLAG_OFFSET = BigInt(1) << BigInt(32);
diff --git a/src/util/schemas/UsernameAttemptUnauthedSchema.ts b/src/util/schemas/UsernameAttemptUnauthedSchema.ts
new file mode 100644
index 00000000..0ac83dd0
--- /dev/null
+++ b/src/util/schemas/UsernameAttemptUnauthedSchema.ts
@@ -0,0 +1,3 @@
+export interface UsernameAttemptUnauthedSchema {
+ username: string;
+}
diff --git a/src/util/schemas/index.ts b/src/util/schemas/index.ts
index 44a504cd..bb449e45 100644
--- a/src/util/schemas/index.ts
+++ b/src/util/schemas/index.ts
@@ -72,6 +72,7 @@ export * from "./UserModifySchema";
export * from "./UserNoteUpdateSchema";
export * from "./UserProfileModifySchema";
export * from "./UserSettingsSchema";
+export * from "./UsernameAttemptUnauthedSchema";
export * from "./Validator";
export * from "./VanityUrlSchema";
export * from "./VoiceIdentifySchema";
diff --git a/src/util/schemas/responses/UsernameAttemptResponse.ts b/src/util/schemas/responses/UsernameAttemptResponse.ts
new file mode 100644
index 00000000..864a3bb0
--- /dev/null
+++ b/src/util/schemas/responses/UsernameAttemptResponse.ts
@@ -0,0 +1,3 @@
+export interface UsernameAttemptResponse {
+ taken: boolean;
+}
|