Skip to content

Conversation

@NikD0R
Copy link

@NikD0R NikD0R commented Dec 25, 2025

No description provided.

Copy link

@mateacademy-ai-mentor mateacademy-ai-mentor left a 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:

  1. Missing Activation Check: In backend/src/api/controllers/auth.controller.js, the login function must verify that user.activationToken is null before generating tokens. Currently, unactivated users can log in, which goes against the task requirements.
  2. Missing 404 Handler: The backend server in backend/src/createServer.js needs a catch-all middleware to handle requests to undefined routes and respond with a 404 status.
  3. 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 in backend/src/services/jwt.service.js.
    • Add .env* to the frontend_app/.gitignore file to prevent committing sensitive credentials.
  4. Incorrect Logout Behavior: In frontend_app/src/components/AuthContext.tsx, the client-side state (access token, user) should be cleared immediately within the logout function, 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

  1. Rate AI review example

  2. Re-request AI review example

  3. Add AI review comments example

Comment on lines 29 to 31
if (!email) {
throw ApiError.badRequest('Email isn`t valid');
}

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.

Comment on lines 57 to 59
if (!password || !confirmPassword) {
throw ApiError.badRequest('All fields are required');
}

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)) {

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.

Comment on lines 66 to 81
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);

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)) {

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');

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();

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));

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)

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.

Comment on lines 17 to 22
static unathorized(errors) {
return new ApiError({
message: 'Unathorized user',
errors,
status: 401,
})

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.

}

function signRefresh(user) {
const token = jwt.sign(user, process.env.JWT_REFRESH_KEY);

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`;

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.

Comment on lines 16 to 17
function normalize({ id, email }) {
return { id, email };

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.

const repeteadEmail = await User.findOne({ where: { email: newEmail, id: { [Op.ne]: userId } } });

if (repeteadEmail) {
throw ApiError.badRequest();

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.'
});

host: DB_HOST || 'localhost',
dialect: 'postgres',
port: DB_PORT || 5432,
password: DB_PASSWORD || '12ND1205',

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

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.

Comment on lines 55 to 59
async function logout() {
await authService.logout();

accessTokenService.remove();
setCurrentUser(null);

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.

const handleLogout = () => {
logout()
.then(() => {
navigate("/");

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.

Comment on lines 113 to 118
<Route path="sign-up" element={<RegistrationPage />} />
<Route path="login" element={<LoginPage />} />
<Route
path="activation/:email/:activationToken"
element={<AccountActivationPage />}
/>

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');

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.

Comment on lines 49 to 59
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));

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.

Comment on lines +166 to +174
{(errors.oldPassword ||
errors.newPassword ||
errors.confirmPassword) && (
<p className="help is-danger">
{errors.oldPassword ||
errors.newPassword ||
errors.confirmPassword}
</p>
)}

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)) {

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.

Copy link

@mateacademy-ai-mentor mateacademy-ai-mentor left a 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's activationToken is null before 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 .catch blocks 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

  1. Rate AI review example

  2. Re-request AI review example

  3. Add AI review comments example

throw ApiError.badRequest('Wrong password');
}

await generateTokens(res, user);

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.

Comment on lines 39 to 40
if (errors.name) {
throw ApiError.badRequest('Invalid name');

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.

Comment on lines 90 to 91
if (newEmail !== confirmEmail) {
throw ApiError.badRequest('Emails don`t match');

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',
});

Comment on lines 17 to 22
static unathorized(errors) {
return new ApiError({
message: 'Unathorized user',
errors,
status: 401,
})

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 } } });

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.

Comment on lines +43 to +49
logout()
.then(() => {
navigate("/login");
})
.catch((error: AxiosError<{ message?: string }>) => {
setError(error.response?.data?.message ?? "");
});

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}

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"))

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.

Comment on lines 113 to 114
.catch(() =>
helpers.setFieldError("oldPassword", "Wrong old password")

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"))

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.

Copy link

@mateacademy-ai-mentor mateacademy-ai-mentor left a 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

  1. Rate AI review example

}

if (errors.name) {
throw ApiError.badRequest('Invalid name');

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();

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) {

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.


static unathorized(errors) {
return new ApiError({
message: 'Unathorized user',

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.

Comment on lines 30 to 31
throw ApiError.badRequest('User is already existed', {
email: 'User is already existed'

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".

const match = await bcrypt.compare(password, user.password);

if (!match) {
throw ApiError.unathorized();

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().

Comment on lines 113 to 118
<Route path="sign-up" element={<RegistrationPage />} />
<Route path="login" element={<LoginPage />} />
<Route
path="activation/:email/:activationToken"
element={<AccountActivationPage />}
/>

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");

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}

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants