Skip to content

Commit

Permalink
feat: improve sign in error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielmeloc22 committed Aug 14, 2024
1 parent 06707bd commit 2fd95d4
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 59 deletions.
8 changes: 7 additions & 1 deletion apps/server/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -321,11 +321,17 @@ A field whose value conforms to the standard internet email address format as sp
scalar EmailAddress @specifiedBy(url: "https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address")

type LoginMutationPayload {
token: String!
token: String
user: User
errors: [LoginMutationErrors!]
clientMutationId: String
}

enum LoginMutationErrors {
INVALID_CREDENTIALS
INVALID_PASSWORD
}

input LoginMutationInput {
password: NonEmptyString
email: String
Expand Down
98 changes: 59 additions & 39 deletions apps/server/src/modules/user/mutations/LoginMutation.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,76 @@
import { generateToken } from "@/auth";
import type { Context } from "@/context";
import { compare } from "bcrypt";
import { GraphQLNonNull, GraphQLString } from "graphql";
import {
GraphQLEnumType,
GraphQLList,
GraphQLNonNull,
GraphQLString,
} from "graphql";
import { mutationWithClientMutationId } from "graphql-relay";
import { GraphQLNonEmptyString } from "graphql-scalars";
import { type User, UserModel } from "../UserModel";
import { UserType } from "../UserType";

type LoginInput = {
username?: string;
email?: string;
password: string;
username?: string;
email?: string;
password: string;
};

type LoginErrors = "INVALID_CREDENTIALS" | "INVALID_PASSWORD";

type LoginOutput = {
user: User;
token: string;
user: User | null;
token: string | null;
errors: LoginErrors[] | null;
};

export const Login = mutationWithClientMutationId<
LoginInput,
Promise<LoginOutput>,
Context
LoginInput,
Promise<LoginOutput>,
Context
>({
name: "LoginMutation",
inputFields: {
password: { type: GraphQLNonEmptyString },
email: { type: GraphQLString },
username: { type: GraphQLString },
},
outputFields: {
token: { type: new GraphQLNonNull(GraphQLString) },
user: { type: UserType },
},
mutateAndGetPayload: async ({ email, username, password }) => {
const user = await UserModel.findOne({
$or: [
{ email: email?.toLowerCase()?.trim() },
{ username: username?.trim() },
],
}).select("+password");

if (!user) {
throw new Error("Invalid email or username");
}

if (!(await compare(password, user.password))) {
throw new Error("Invalid password");
}

const token = generateToken(user);

return { token, user };
},
name: "LoginMutation",
inputFields: {
password: { type: GraphQLNonEmptyString },
email: { type: GraphQLString },
username: { type: GraphQLString },
},
outputFields: {
token: { type: GraphQLString },
user: { type: UserType },
errors: {
type: new GraphQLList(
new GraphQLNonNull(
new GraphQLEnumType({
name: "LoginMutationErrors",
values: {
INVALID_CREDENTIALS: { value: "INVALID_CREDENTIALS" },
INVALID_PASSWORD: { value: "INVALID_PASSWORD" },
},
}),
),
),
},
},
mutateAndGetPayload: async ({ email, username, password }) => {
const cleanEmail = email?.toLowerCase()?.trim();
const cleanUsername = username?.trim();

const user = await UserModel.findOne({
$or: [{ email: cleanEmail }, { username: cleanUsername }],
}).select("+password");

if (!user) {
return { errors: ["INVALID_CREDENTIALS"], token: null, user: null };
}
if (!(await compare(password, user.password))) {
return { errors: ["INVALID_PASSWORD"], token: null, user: null };
}

const token = generateToken(user);

return { token, user, errors: null };
},
});
19 changes: 14 additions & 5 deletions apps/web/__generated__/signInMutation.graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion apps/web/data/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -321,11 +321,17 @@ A field whose value conforms to the standard internet email address format as sp
scalar EmailAddress @specifiedBy(url: "https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address")

type LoginMutationPayload {
token: String!
token: String
user: User
errors: [LoginMutationErrors!]
clientMutationId: String
}

enum LoginMutationErrors {
INVALID_CREDENTIALS
INVALID_PASSWORD
}

input LoginMutationInput {
password: NonEmptyString
email: String
Expand Down
31 changes: 18 additions & 13 deletions apps/web/src/components/sign-in.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const SignInMutation = graphql`
mutation signInMutation($input: LoginMutationInput!) {
login(input: $input) {
token
errors
}
}
`;
Expand All @@ -50,23 +51,27 @@ export function SignIn() {
variables: {
input: { username: data.credential, password: data.password },
},
// updater: (store, data) => {
// if (data?.login?.token) {
// store.invalidateStore();
// }
// },
onCompleted: (data, errors) => {
onCompleted: (data) => {
if (data.login?.token) {
login(data.login.token);
location.reload();
}
if (errors) {
// TODO: improve error handling with server codes
const message = errors[0]?.message;
form.setError(
message?.includes("password") ? "password" : "credential",
{ message },
);

if (data.login?.errors) {
for (const error of data.login.errors) {
switch (error) {
case "INVALID_CREDENTIALS":
form.setError("credential", {
message: "Invalid e-mail or username",
});
break;
case "INVALID_PASSWORD":
form.setError("password", {
message: "Invalid password",
});
break;
}
}
}
},
});
Expand Down

0 comments on commit 2fd95d4

Please sign in to comment.