From 4d027a424e442f10371d205e68aa3bf63d0eba0e Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Fri, 8 Sep 2023 21:33:04 -0400 Subject: [PATCH] Implement Pomelo Registration --- assets/locales/en/auth.json | 3 +- assets/openapi.json | Bin 575462 -> 577326 bytes assets/schemas.json | Bin 18718290 -> 18699325 bytes src/api/routes/auth/register.ts | 11 ++++- .../username-attempt-unauthed.ts | 33 +++++++++++++++ .../username-suggestions-unauthed.ts | 37 ++++++++++++++++ src/api/routes/users/@me/index.ts | 6 +-- src/util/entities/User.ts | 40 ++++++++++++++++++ .../schemas/UsernameAttemptUnauthedSchema.ts | 3 ++ src/util/schemas/index.ts | 1 + .../responses/UsernameAttemptResponse.ts | 3 ++ 11 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 src/api/routes/unique-username/username-attempt-unauthed.ts create mode 100644 src/api/routes/unique-username/username-suggestions-unauthed.ts create mode 100644 src/util/schemas/UsernameAttemptUnauthedSchema.ts create mode 100644 src/util/schemas/responses/UsernameAttemptResponse.ts diff --git a/assets/locales/en/auth.json b/assets/locales/en/auth.json index 2415c657c..a4595662b 100644 --- a/assets/locales/en/auth.json +++ b/assets/locales/en/auth.json @@ -15,7 +15,8 @@ "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", - "TOO_MANY_REGISTRATIONS": "Too many registrations, please try again later" + "TOO_MANY_REGISTRATIONS": "Too many registrations, please try again later", + "UNIQUE_USERNAMES_DISABLED": "The Unique Usernames feature is disabled on this instance" }, "password_reset": { "EMAIL_DOES_NOT_EXIST": "Email does not exist.", diff --git a/assets/openapi.json b/assets/openapi.json index 7b2243e9cd815e798e1eab6fb6059be6ced1d651..c3e1419dd51b37b25225afcf02a85a9c3ba66c0e 100644 GIT binary patch delta 682 zcmaFXue@%aa>EwJ+ZU#9IL6F3T|k9JX8M6FCfn%;LYXxu>DWzvuPkiMgqcB_*l31tp<*iKQhOsVUPxUS{f;zTgW} z&UA-sj27Ds6`2qA60l)EF98L2*(Xon=Gs2x1oPocel&}x3%W6gajeNvO*qK-RTpivouYg@Sc%h&N(AB zIa?t!O(9VM=17IiVui%A#LS$;q{$NlL?<_hvra#7lI8j2gdP@QEYY%kqch7)Ms6gT z>5OqKleQb|;h4iX{ew2E+;qRmOl(Nb72BS}!^&?rJ>UY1`}Re_tTK$-4|uVPa0%su zLc~Zvv7jI)GdU3yOxxq9vz%v~9w5!AF#W;;W}fK+R%|5@LwCBe+O}T_VcmWugl*F6 zc4tra?arPYiI$SiL9UJ=uJJ*x?w-LRL5?Aw{(kWx{@$*B(=Xm*o4GyV0-F@$_P7>~ G4rc&h4+b~@ delta 290 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 BRRsV5 delta 3725 zcmbtX2~bp57Of97G%YAw*bP5FaSAj=YG* z5C%cQ1PLQtL9j7INhu}7QCVag_As)Jrp6e@`E*AJu-%N?Ih?g zFam#paS-Ve$tIbAY?5gnW!_)468fvmgk+{TCgU48DUT&dA z+Mu-c??BpmO9d(y7F0mpQC83$Wo>z#h!Bez|0)~sud*Hgo?uYs=ShS4 zwqB5L>;2Dq(!43y2bzL?6DaePfftQyP(d^oyP$S`XU7-oHxO3jDZ`E+9-Og9FVwh}IowrLn> zn}$Cmg^=gY2=Lsw*OBZCx|6vN?qo)OMGB+l#*d-7aep0IQ(O~qoQpjlxtSIYsjkB+ zAwEupDyng4XqOBO?UEgmg$(6sl;TQ|FJssq>%F&Q{$^hgJ8EhLep)4>-r*0SARr{mZh?ge?24eo_~)b%UCS z6K>K7*xPyW!a@R^4FY^phir(1VXrc{cr&+`6p}FAPx7GqN&dDLqTCWD)}|0*ZT`}+ z)K5Xc=Km~H-9$zHFPKc9CY%( z99SCvGp`bO=Jl03HCp0Win2NB#CcdqosLde5M(lCu(5m}}97VjSfo^TDhg;j>V^STZ9l8u@hZ?9a zC1ybmGOlU_o`&vy3vORSqu|< z{W9f-FD^{EA5#+5E43WG^|EHH;q7lHDxDW(~TMx;&V=AMKV9)m?X>y6YbnQi8ow z%;fKZnf(1o3LIS11%qq411T^&qzAG?dV?v@so()P74*^IR7*chwG6~iC(CCCp?r4e zu5<-qy|mqpP)hlwx@;#8qSi3@3v**X5PD!2j>s2 zeII*IXD8v3F`22t79|dp@KjQAu+?0s0@es9(S+xk0p1+P^^%A0;NY;D4nY-U7tt$o^*8HGR(Q z9rE0kdy+$51=*v$OP?n-(Hho&SKV&XW+S#)uHj z3VwlE!BYqFidx)4xF|XCS4d8LRw}(j*l&|~t%$tvUx>W${JLb1Tx^W*tNVB@Pg!$u zY*t}@sM$K`T+o>y+a^{!lRC~QicGX+neyLnR67@28I?9+D@V15ubaxaqTdV*&a+f? z|04}vOaWX{qf#WfpJ1aEu|LK~T3Wi#(xh@1lVUTIV>HR?N*0>jlWrVFbka?pSwj_= zU1^kawG$N>k#-`NA-3UBVQx)@A|tA);4xNY3v { 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 000000000..a1f63a691 --- /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 000000000..9b112b558 --- /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 f4578126c..55d2ce122 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 d1fbb5c2b..5ec9862e4 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 000000000..0ac83dd0e --- /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 44a504cda..bb449e450 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 000000000..864a3bb00 --- /dev/null +++ b/src/util/schemas/responses/UsernameAttemptResponse.ts @@ -0,0 +1,3 @@ +export interface UsernameAttemptResponse { + taken: boolean; +}