From 648d29681f2b7e000c275585843ac45fae97c043 Mon Sep 17 00:00:00 2001 From: gabrielmeloc22 Date: Wed, 14 Aug 2024 20:41:26 -0300 Subject: [PATCH] feat: improve sign up error handling --- apps/server/schema/schema.graphql | 6 ++ .../user/mutations/RegisterMutation.ts | 60 ++++++++++++++----- .../__generated__/signUpMutation.graphql.ts | 17 ++++-- apps/web/data/schema.graphql | 6 ++ apps/web/src/components/sign-up.tsx | 20 +++++-- 5 files changed, 85 insertions(+), 24 deletions(-) diff --git a/apps/server/schema/schema.graphql b/apps/server/schema/schema.graphql index 504571d..ce8ec78 100644 --- a/apps/server/schema/schema.graphql +++ b/apps/server/schema/schema.graphql @@ -305,9 +305,15 @@ type mutation { type RegisterUserPayload { token: String user: User + errors: [RegisterMutationErrors!] clientMutationId: String } +enum RegisterMutationErrors { + USERNAME_TAKEN + EMAIL_TAKEN +} + input RegisterUserInput { username: String! email: EmailAddress diff --git a/apps/server/src/modules/user/mutations/RegisterMutation.ts b/apps/server/src/modules/user/mutations/RegisterMutation.ts index fdb7268..67aabf1 100644 --- a/apps/server/src/modules/user/mutations/RegisterMutation.ts +++ b/apps/server/src/modules/user/mutations/RegisterMutation.ts @@ -1,7 +1,12 @@ import { generateToken } from "@/auth"; import type { Context } from "@/context"; -import { genSaltSync, hashSync } from "bcrypt"; -import { GraphQLNonNull, GraphQLString } from "graphql"; +import { genSalt, hash } from "bcrypt"; +import { + GraphQLEnumType, + GraphQLList, + GraphQLNonNull, + GraphQLString, +} from "graphql"; import { mutationWithClientMutationId } from "graphql-relay"; import { EmailAddressResolver } from "graphql-scalars"; import { type User, UserModel } from "../UserModel"; @@ -13,9 +18,12 @@ type RegisterInput = { password: string; }; +type RegisterError = "USERNAME_TAKEN" | "EMAIL_TAKEN"; + type RegisterOutput = { token: string | null; user: User | null; + errors: RegisterError[] | null; }; export const Register = mutationWithClientMutationId< @@ -31,6 +39,19 @@ export const Register = mutationWithClientMutationId< user: { type: UserType, }, + errors: { + type: new GraphQLList( + new GraphQLNonNull( + new GraphQLEnumType({ + name: "RegisterMutationErrors", + values: { + USERNAME_TAKEN: { value: "USERNAME_TAKEN" }, + EMAIL_TAKEN: { value: "EMAIL_TAKEN" }, + }, + }), + ), + ), + }, }, inputFields: { username: { type: new GraphQLNonNull(GraphQLString) }, @@ -38,24 +59,33 @@ export const Register = mutationWithClientMutationId< password: { type: new GraphQLNonNull(GraphQLString) }, }, mutateAndGetPayload: async ({ email, password, username }) => { - const userAlreadyExists = !!(await UserModel.findOne({ - $or: [ - { email: email.trim().toLowerCase() }, - { username: username.trim() }, - ], - })); + const cleanUsername = username.trim(); + const cleanEmail = email.trim().toLowerCase(); + + const duplicateUser = await UserModel.findOne({ + $or: [{ email: cleanEmail }, { username: cleanUsername }], + }); + + if (duplicateUser) { + const errors: RegisterError[] = []; + + if (duplicateUser.email === cleanEmail) { + errors.push("EMAIL_TAKEN"); + } + if (duplicateUser.username === cleanUsername) { + errors.push("USERNAME_TAKEN"); + } - if (userAlreadyExists) { - throw new Error("User already exists"); + return { errors, token: null, user: null }; } - const salt = genSaltSync(); - const hashedPassword = hashSync(password, salt); + const salt = await genSalt(); + const hashedPassword = await hash(password, salt); const user = await UserModel.create({ - email: email.trim(), + email: cleanEmail, password: hashedPassword, - username: username.trim(), + username: cleanUsername, }); const token = generateToken(user); @@ -65,6 +95,6 @@ export const Register = mutationWithClientMutationId< throw new Error(JSON.stringify(err)); } - return { token, user }; + return { token, user, errors: null }; }, }); diff --git a/apps/web/__generated__/signUpMutation.graphql.ts b/apps/web/__generated__/signUpMutation.graphql.ts index 5d81eaa..50cbbba 100644 --- a/apps/web/__generated__/signUpMutation.graphql.ts +++ b/apps/web/__generated__/signUpMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<21b95dec261cfa1256fb5c0de7a9010f>> * @lightSyntaxTransform * @nogrep */ @@ -9,6 +9,7 @@ // @ts-nocheck import { ConcreteRequest, Mutation } from 'relay-runtime'; +export type RegisterMutationErrors = "EMAIL_TAKEN" | "USERNAME_TAKEN" | "%future added value"; export type RegisterUserInput = { clientMutationId?: string | null | undefined; email?: any | null | undefined; @@ -20,6 +21,7 @@ export type signUpMutation$variables = { }; export type signUpMutation$data = { readonly register: { + readonly errors: ReadonlyArray | null | undefined; readonly token: string | null | undefined; } | null | undefined; }; @@ -51,6 +53,13 @@ v1 = [ "name": "register", "plural": false, "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "errors", + "storageKey": null + }, { "alias": null, "args": null, @@ -80,16 +89,16 @@ return { "selections": (v1/*: any*/) }, "params": { - "cacheID": "66dd23d210dcae92f1ea1c34e58b6a99", + "cacheID": "a6c30a7b101ecbb9544722512c98f18c", "id": null, "metadata": {}, "name": "signUpMutation", "operationKind": "mutation", - "text": "mutation signUpMutation(\n $input: RegisterUserInput!\n) {\n register(input: $input) {\n token\n }\n}\n" + "text": "mutation signUpMutation(\n $input: RegisterUserInput!\n) {\n register(input: $input) {\n errors\n token\n }\n}\n" } }; })(); -(node as any).hash = "7ccb1de92d60cc47ad30bd70a4a99d20"; +(node as any).hash = "b0598d4169075ac85d3ee1b413d17191"; export default node; diff --git a/apps/web/data/schema.graphql b/apps/web/data/schema.graphql index 504571d..ce8ec78 100644 --- a/apps/web/data/schema.graphql +++ b/apps/web/data/schema.graphql @@ -305,9 +305,15 @@ type mutation { type RegisterUserPayload { token: String user: User + errors: [RegisterMutationErrors!] clientMutationId: String } +enum RegisterMutationErrors { + USERNAME_TAKEN + EMAIL_TAKEN +} + input RegisterUserInput { username: String! email: EmailAddress diff --git a/apps/web/src/components/sign-up.tsx b/apps/web/src/components/sign-up.tsx index 6b1647d..01eda84 100644 --- a/apps/web/src/components/sign-up.tsx +++ b/apps/web/src/components/sign-up.tsx @@ -27,6 +27,7 @@ const UserRegisterFormSchema = z.object({ const RegisterUserMutation = graphql` mutation signUpMutation($input: RegisterUserInput!) { register(input: $input) { + errors token } } @@ -39,16 +40,25 @@ export function SignUp() { const onSubmit = form.handleSubmit((data) => { register({ variables: { input: data }, - updater: (store, data) => { - if (data?.register?.token) { - store.invalidateStore(); - } - }, onCompleted: (data) => { if (data.register?.token) { login(data.register.token); location.reload(); } + if (data.register?.errors) { + for (const error of data.register.errors) { + switch (error) { + case "EMAIL_TAKEN": + form.setError("email", { message: "Email already taken" }); + break; + case "USERNAME_TAKEN": + form.setError("username", { + message: "Username already taken", + }); + break; + } + } + } }, }); });