-
Notifications
You must be signed in to change notification settings - Fork 299
Task solution #216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Task solution #216
Conversation
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review Summary
This is a very solid full-stack implementation, and you've correctly built most of the core features. However, there are a few critical issues that prevent the project from meeting all requirements, so I'm requesting some changes before approval.
Here are the main points to address:
- Missing Activation Check: In
backend/src/api/controllers/auth.controller.js, theloginfunction must verify thatuser.activationTokenisnullbefore generating tokens. Currently, unactivated users can log in, which goes against the task requirements. - Missing 404 Handler: The backend server in
backend/src/createServer.jsneeds a catch-all middleware to handle requests to undefined routes and respond with a 404 status. - Security Vulnerabilities: Please fix these critical security issues:
- Remove the hardcoded fallback password in
backend/src/utils/db.js. - Add an expiration time (e.g.,
expiresIn: '30d') to refresh tokens inbackend/src/services/jwt.service.js. - Add
.env*to thefrontend_app/.gitignorefile to prevent committing sensitive credentials.
- Remove the hardcoded fallback password in
- Incorrect Logout Behavior: In
frontend_app/src/components/AuthContext.tsx, the client-side state (access token, user) should be cleared immediately within thelogoutfunction, even if the API call to the server fails. This ensures the user is logged out on the frontend regardless of network issues.
Once these key issues are resolved, your project will be in great shape. Keep up the great work!
✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨
Important
Fix the issues, commit the changes, and then re-request my review using the Re-request review button2 in the Reviewers tab.
Tip
If you have any comments about this review, please leave a message in the Add a comment section below and mention me with the @mateacademy-ai-mentor tag3.
I will read your message during my next review of your pull request. Please do not reply directly to my comments, as that feature is not supported yet.
Caution
Do not close the pull request until the review is complete. Closing it early can lead to unexpected behavior and disrupt the review process.
Footnotes
| if (!email) { | ||
| throw ApiError.badRequest('Email isn`t valid'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check is redundant. The validateEmail function called on line 22 already handles the case where the email is empty. If the email is missing, an error will be thrown on line 26, and this code will never be reached.
| if (!password || !confirmPassword) { | ||
| throw ApiError.badRequest('All fields are required'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check is redundant. The validatePassword function, called for both password and confirmPassword on lines 49-50, already handles cases where these fields are empty. An error is thrown on line 54 if either is missing, so this block is unreachable.
| return "Name is required"; | ||
| } | ||
|
|
||
| if (name.length < 2 && !NAME_PATTERN.test(name)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logical AND (&&) in this condition seems incorrect. For example, an invalid name like J@hn would pass this check because its length is greater than 2. The regular expression already enforces a minimum length of 2, so the name.length < 2 check is redundant and causes this logical issue.
| const login = async (req, res) => { | ||
| const { email, password } = req.body; | ||
|
|
||
| const user = await userServices.findByEmail(email); | ||
|
|
||
| if (!user) { | ||
| throw ApiError.badRequest('No such user'); | ||
| } | ||
|
|
||
| const isPasswordValid = await bcrypt.compare(password, user.password); | ||
|
|
||
| if (!isPasswordValid) { | ||
| throw ApiError.badRequest('Wrong password'); | ||
| } | ||
|
|
||
| await generateTokens(res, user); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The login function is missing a crucial check required by the task. Before generating tokens, you must verify if the user has activated their account. You can do this by checking if user.activationToken is null. If it's not null, you should return an error prompting the user to activate their email.
| return "Name is required"; | ||
| } | ||
|
|
||
| if (name.length < 2 && !NAME_PATTERN.test(name)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logical operator && in this condition is incorrect. A name that is long enough (e.g., more than 2 characters) but contains invalid characters will incorrectly pass this validation because the first part of the condition (name.length < 2) will be false. You should use || to ensure that the name is checked against the pattern regardless of its length.
| } | ||
|
|
||
| if (errors.name) { | ||
| throw ApiError.badRequest('Invalid name'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When throwing this error, you are losing the specific validation message generated by validateName. It's better to include the errors object in the ApiError.badRequest call, similar to how it's done in the changePassword function. This provides more useful feedback to the client.
| } | ||
|
|
||
| if (errors.password || errors.newEmail || errors.confirmEmail) { | ||
| throw ApiError.badRequest(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ApiError.badRequest() call doesn't pass the validation errors object to the client. As a result, the user won't know which field is invalid. You should include the errors object to provide specific feedback, like you do in the changePassword function.
| export const authRouter = Router(); | ||
|
|
||
| authRouter.post('/registration', catchError(authController.register)); | ||
| authRouter.get('/activation/:email/:activationToken', catchError(authController.activate)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This route is defined with both :email and :activationToken parameters. However, the corresponding authController.activate function only uses the activationToken. To keep the API contract clear and consistent, you should either update the controller to also use the email parameter for validation or remove the :email parameter from the route if it's not needed.
| res.send('Server is active'); | ||
| }); | ||
|
|
||
| app.use(errorMiddleware) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The task requires returning a 404 for all pages that are not explicitly defined. This implementation is missing a catch-all middleware to handle requests to undefined routes. You should add a middleware here, after all your routers but before the errorMiddleware, to catch any remaining requests and respond with a 404 status.
| static unathorized(errors) { | ||
| return new ApiError({ | ||
| message: 'Unathorized user', | ||
| errors, | ||
| status: 401, | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a typo in the method name. It should be unauthorized instead of unathorized. The message 'Unathorized user' on line 19 has the same typo. Correcting this will improve the code's readability and correctness.
backend/src/services/jwt.service.js
Outdated
| } | ||
|
|
||
| function signRefresh(user) { | ||
| const token = jwt.sign(user, process.env.JWT_REFRESH_KEY); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refresh tokens should have an expiration time to limit the window of opportunity for an attacker if a token is compromised. This token is being signed without an expiresIn option, making it valid indefinitely. You should add an expiration that matches the cookie's maxAge, such as expiresIn: '30d'.
| } | ||
|
|
||
| function sendNotificationEmail(email) { | ||
| const href = `${process.env.CLIENT_HOST}/profile/email`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The href variable is defined here but it's not used in the HTML template below. You can either use it to provide a helpful link in the email or remove this line to clean up the code.
backend/src/services/user.service.js
Outdated
| function normalize({ id, email }) { | ||
| return { id, email }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This normalize function only returns the id and email. The task requires a profile page that should display the user's name. When a user logs in or refreshes their session, the user object sent to the client is normalized here, meaning the client will not receive the user's name. You should include name in the returned object to ensure the profile page can display it.
backend/src/services/user.service.js
Outdated
| const repeteadEmail = await User.findOne({ where: { email: newEmail, id: { [Op.ne]: userId } } }); | ||
|
|
||
| if (repeteadEmail) { | ||
| throw ApiError.badRequest(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When throwing this ApiError, you are not providing any specific error message. The client will only receive a generic 'Bad request' response, which is not helpful for the user. You should include a descriptive message and an errors object, for example:
throw ApiError.badRequest('Validation error', {
email: 'This email is already in use.'
});
backend/src/utils/db.js
Outdated
| host: DB_HOST || 'localhost', | ||
| dialect: 'postgres', | ||
| port: DB_PORT || 5432, | ||
| password: DB_PASSWORD || '12ND1205', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoding a fallback password is a significant security risk. Sensitive credentials should never be committed to source control. Please remove this hardcoded value and rely exclusively on the DB_PASSWORD environment variable.
| node_modules | ||
| dist | ||
| dist-ssr | ||
| *.local |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is missing an important entry to ignore environment files. Files like .env often contain sensitive credentials and should not be committed to version control. You should add a line to ignore all .env files (e.g., .env*) to prevent accidental exposure of secrets, while potentially allowing template files like !.env.example.
| async function logout() { | ||
| await authService.logout(); | ||
|
|
||
| accessTokenService.remove(); | ||
| setCurrentUser(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the authService.logout() call fails (e.g., due to a network error), an exception will be thrown, and the lines to remove the access token and clear the currentUser will not be executed. This leaves the user logged in on the client side, which is incorrect. The client-side state should be cleared regardless of the server's response to ensure a consistent user experience. Consider moving the state-clearing logic into a finally block or executing it before awaiting the API call.
frontend_app/src/App.tsx
Outdated
| const handleLogout = () => { | ||
| logout() | ||
| .then(() => { | ||
| navigate("/"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the task requirements, the application should "Redirect to login after logging out". Currently, it redirects to the home page (/). Please update this to navigate to the login page.
frontend_app/src/App.tsx
Outdated
| <Route path="sign-up" element={<RegistrationPage />} /> | ||
| <Route path="login" element={<LoginPage />} /> | ||
| <Route | ||
| path="activation/:email/:activationToken" | ||
| element={<AccountActivationPage />} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For better consistency and to avoid duplicating logic, consider moving the routes for registration, login, and activation inside the <Route element={<RequireNonAuth />}> wrapper. This would centralize the access control for non-authenticated pages, similar to how the password reset routes are handled.
|
|
||
| // add `Authorization` header to all requests | ||
| httpClient.interceptors.request.use(request => { | ||
| const accessToken = localStorage.getItem('accessToken'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line directly accesses localStorage to retrieve the access token. To maintain consistency and respect the abstraction provided by your services, you should use accessTokenService.get() here. This ensures that all interactions with token storage are centralized within accessTokenService.
| onSubmit={({ name }, helpers) => { | ||
| setNameSuccess(""); | ||
|
|
||
| authService | ||
| .changeName(name) | ||
| .then(() => { | ||
| helpers.resetForm({ values: { name } }); | ||
| showSuccess(setNameSuccess, "Name updated successfully"); | ||
| }) | ||
| .catch(() => helpers.setFieldError("name", "Failed to update name")) | ||
| .finally(() => helpers.setSubmitting(false)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After a successful name change, the currentUser object in the AuthContext is not updated. This causes the UI, such as the user's name in the navigation bar, to display stale information until the page is reloaded. To fix this, the AuthContext should expose a function to update the currentUser state, which should be called here with the updated user data returned from the API.
| {(errors.oldPassword || | ||
| errors.newPassword || | ||
| errors.confirmPassword) && ( | ||
| <p className="help is-danger"> | ||
| {errors.oldPassword || | ||
| errors.newPassword || | ||
| errors.confirmPassword} | ||
| </p> | ||
| )} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error display for the password change form is not ideal. It shows only one error message for all three fields combined, making it difficult for the user to identify which field is invalid. For a better user experience, each field (oldPassword, newPassword, confirmPassword) should have its own dedicated error message displayed below it, similar to the "Change name" form. The "Change email" form has the same issue.
| return "Name is required"; | ||
| } | ||
|
|
||
| if (name.length < 2 && !NAME_PATTERN.test(name)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic in this condition is incorrect. The NAME_PATTERN regular expression already checks for a minimum length of 2 characters. Because of the && operator, a name that is long enough but contains invalid characters (e.g., J@hn) will incorrectly pass this validation. You should remove the name.length < 2 check and only validate against the pattern.
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review Summary
Excellent progress on the application! You've addressed most of the previous feedback, and the overall structure is very solid. However, a few changes are needed to address a critical requirement before approval.
The most important issue is in the login flow. Currently, a user can log in without activating their account, which goes against the task requirements.
- In
backend/src/api/controllers/auth.controller.js, please add a check to ensure a user'sactivationTokenisnullbefore generating tokens and completing the login.
Additionally, the error handling on the profile page should be more specific:
- In
frontend_app/src/pages/ProfilePage.tsx, the.catchblocks for name, password, and email changes use generic messages. This can be misleading. Please update the logic to parse the actual error response from the server to provide accurate feedback to the user.
Once these key functional issues are resolved, your project will be in great shape. Keep up the fantastic work!
✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨
Important
Fix the issues, commit the changes, and then re-request my review using the Re-request review button2 in the Reviewers tab.
Tip
If you have any comments about this review, please leave a message in the Add a comment section below and mention me with the @mateacademy-ai-mentor tag3.
I will read your message during my next review of your pull request. Please do not reply directly to my comments, as that feature is not supported yet.
Caution
Do not close the pull request until the review is complete. Closing it early can lead to unexpected behavior and disrupt the review process.
Footnotes
| throw ApiError.badRequest('Wrong password'); | ||
| } | ||
|
|
||
| await generateTokens(res, user); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the task requirements, a user should not be able to log in if their account is not yet activated. You need to add a check here to ensure that user.activationToken is null before proceeding to generate tokens. Currently, a user can log in without activating their email.
| if (errors.name) { | ||
| throw ApiError.badRequest('Invalid name'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error response here is inconsistent with other validation checks in your application. Instead of a generic message like 'Invalid name', it's better to pass the errors object to provide more specific feedback to the frontend.
Consider changing this to throw ApiError.badRequest('Bad request', errors); to align with the approach used in the changePassword and changeEmail functions.
| if (newEmail !== confirmEmail) { | ||
| throw ApiError.badRequest('Emails don`t match'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency with the changePassword controller, it would be better to return a structured error that specifies which field has the error. This helps the frontend display the error message next to the correct input field.
Consider changing this to something like:
throw ApiError.badRequest('Emails do not match', {
confirmEmail: 'Emails do not match',
});| static unathorized(errors) { | ||
| return new ApiError({ | ||
| message: 'Unathorized user', | ||
| errors, | ||
| status: 401, | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a typo here. The correct spelling is "unauthorized". While the code works because you've used this spelling consistently, it's good practice to correct such typos for better code clarity and maintainability. You'll need to update this in the method name, the message, and then in all the places where this method is called.
| throw ApiError.unathorized(); | ||
| } | ||
|
|
||
| const repeteadEmail = await User.findOne({ where: { email: newEmail, id: { [Op.ne]: userId } } }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a typo in this variable name. It should be repeatedEmail.
| logout() | ||
| .then(() => { | ||
| navigate("/login"); | ||
| }) | ||
| .catch((error: AxiosError<{ message?: string }>) => { | ||
| setError(error.response?.data?.message ?? ""); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You've correctly updated the logout function in AuthContext to clear the local state immediately, which is great! However, the navigation to the login page here only happens if the API call is successful (inside the .then() block).
To fully meet the requirement of redirecting after logout, the user should be navigated to the login page even if the network call to the server fails, since their client-side session has been cleared. Consider moving the navigate("/login") call to a .finally() block to ensure it always runs.
| className={cn("button is-success has-text-weight-bold", { | ||
| "is-loading": isSubmitting, | ||
| })} | ||
| disabled={isSubmitting || !!errors.email || !!errors.password} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The disabled condition for the submit button is missing a check for !!errors.name. To ensure the button is disabled whenever there's any validation error on the form, you should add !!errors.name to this condition.
| helpers.resetForm({ values: { name } }); | ||
| showSuccess(setNameSuccess, "Name updated successfully"); | ||
| }) | ||
| .catch(() => helpers.setFieldError("name", "Failed to update name")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling here is too generic. If the server returns a specific validation error (e.g., the name is invalid), this catch block will always show "Failed to update name". You should parse the actual error response from the server to provide more accurate feedback to the user, similar to how you handle errors on the registration page.
| .catch(() => | ||
| helpers.setFieldError("oldPassword", "Wrong old password") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This .catch block assumes that any error from the API is due to a wrong old password. However, the backend might return other validation errors, such as for the newPassword or confirmPassword fields. The error handling should be updated to parse the response from the server and set the appropriate field errors dynamically.
| "Email changed successfully. Please check your old email." | ||
| ); | ||
| }) | ||
| .catch(() => helpers.setFieldError("password", "Wrong password")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the other forms on this page, this error handling is too simplistic. It assumes any failure is due to a wrong password. The backend could also return an error if the new email address is already in use. This logic should be updated to inspect the server's error response and display the correct message for the corresponding field.
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review Summary
I am approving your submission to help you move forward, but please take a moment to review the important feedback below. You've done an excellent job building a secure and robust backend, and the frontend user experience is very polished! The main issue to address is with route protection in frontend_app/src/App.tsx; the login, registration, and activation pages should be moved inside the <RequireNonAuth /> component to prevent access by logged-in users. Also, in backend/src/api/controllers/auth.controller.js, you're missing an await on user.save(), which could cause a race condition where the response is sent before the database is updated. Lastly, your CI build is failing because the npm install command needs a working-directory key in your GitHub Actions workflow to run in the backend and frontend_app folders. Addressing these points will make your application even better. Keep up the fantastic work!
✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨
Footnotes
| } | ||
|
|
||
| if (errors.name) { | ||
| throw ApiError.badRequest('Invalid name'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To provide more specific feedback to the client, consider passing the errors object in the ApiError.badRequest call, similar to how it's done in the changePassword and changeEmail functions in this file. This will ensure consistent and detailed error reporting across all profile update actions.
| } | ||
|
|
||
| user.activationToken = null; | ||
| user.save(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The .save() method on a Sequelize model returns a Promise. You should await it to ensure the user's activationToken is updated in the database before sending the response.
| }) | ||
| } | ||
|
|
||
| static unathorized(errors) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a typo in the method name. It should be unauthorized.
backend/src/exceptions/api.error.js
Outdated
|
|
||
| static unathorized(errors) { | ||
| return new ApiError({ | ||
| message: 'Unathorized user', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a typo in the error message. It should be Unauthorized user.
backend/src/services/user.service.js
Outdated
| throw ApiError.badRequest('User is already existed', { | ||
| email: 'User is already existed' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor grammatical suggestion: The error message would be more natural as "User already exists".
backend/src/services/user.service.js
Outdated
| const match = await bcrypt.compare(password, user.password); | ||
|
|
||
| if (!match) { | ||
| throw ApiError.unathorized(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the provided password doesn't match, this is more of a validation error for the request body rather than an authentication failure (since the user is already authenticated via middleware). For consistency with the changePassword function (line 65), consider using ApiError.badRequest('Incorrect password') here instead of ApiError.unathorized().
frontend_app/src/App.tsx
Outdated
| <Route path="sign-up" element={<RegistrationPage />} /> | ||
| <Route path="login" element={<LoginPage />} /> | ||
| <Route | ||
| path="activation/:email/:activationToken" | ||
| element={<AccountActivationPage />} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the task requirements, the registration, login, and activation pages should only be accessible to non-authenticated users. To enforce this, you should move the Route definitions for sign-up, login, and activation inside the <Route element={<RequireNonAuth />}> block below, similar to how the password reset routes are handled.
| accessTokenService.save(accessToken); | ||
| setCurrentUser(user); | ||
| } catch (error) { | ||
| console.log("User is not authentincated"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a small typo here. It should be "authenticated".
| className={cn("button is-success has-text-weight-bold", { | ||
| "is-loading": isSubmitting, | ||
| })} | ||
| disabled={isSubmitting || !!errors.email || !!errors.password} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The disabled check is missing a condition for errors.name. To ensure the button is disabled whenever any field is invalid, you should add !!errors.name to this condition.
No description provided.