Conversation
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Review Summary
I am requesting changes because several core checklist requirements are not yet fully met and there are a couple of technical blockers.
Critical issues that must be fixed:
- The project cannot be built in CI because
npm installfails withENOENTforpackage.json; you need a validpackage.jsonat the repo root (or adjust the workflow to runnpm installin the correct subdirectory) so dependencies can be installed and tests can run at all. - The logout endpoint (
backend/src/routes/authRouter.js,POST /logout) is currently unprotected; the requirement says "Logout (only authenticated)", so this route must be wrapped with yourauthMiddleware(e.g.,authRouter.post('/logout', authMiddleware, ...)) or otherwise reliably return401when no valid access token is provided. - The change‑password flows in both
backend/src/controllers/authController.jsandbackend/src/controllers/userController.jsdo not explicitly validate thatoldPasswordis present before calling the service, but the task requires that changing password must require the current (old) password; add a presence check foroldPasswordand return a structured validation error if it’s missing. - The change‑email flow (in
userController.changeEmailandauthController.changeEmail) updates the email immediately and does not require confirmation of the new email or notify the old email address, which contradicts the requirement that the new email must be confirmed and the old email must be informed; you should add aconfirmation/emailConfirmationfield, validate that it matches, validate the email format, send a confirmation link to the new email, send a notification to the old email, and only switch the stored email after confirmation. - The backend
backend/src/index.jsdoes not install a catch‑all 404 handler, but the checklist requires serving 404 for all other pages; add a final middleware before your error handler likeapp.use((req, res) => res.status(404).send({ message: 'Not found' }))so unknown routes respond correctly.
Minor/robustness suggestions (not blocking):
- On the frontend, several components using
useEffect(e.g.,App.jsx,AccountActivationPage.jsx,PasswordResetPage.jsx) omit dependencies likecheckAuth,activate,navigate, or the route tokens from the dependency array; updating these to include the proper dependencies (or clearly documenting intentional omissions) will avoid React hooks warnings and potential stale‑closure bugs. - For a better UX, validate the presence of
activationTokenandpasswordResetTokenfromuseParams()before firing requests, and show a clear error if they are missing.
Once you address the blockers above, your overall implementation of the auth flows will be much closer to the task’s requirements, and the remaining refinements will be straightforward to tackle.
✨ 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
| async function changePassword(req, res) { | ||
| const { oldPassword, newPassword, confirmation } = req.body; | ||
|
|
||
| const errors = { | ||
| newPassword: validatePassword(newPassword), | ||
| confirmation: | ||
| newPassword !== confirmation ? 'Passwords do not match' : undefined, | ||
| }; | ||
|
|
||
| if (errors.newPassword || errors.confirmation) { | ||
| throw ApiError.BadRequest('Validation error', errors); | ||
| } | ||
|
|
||
| await userService.changePassword(req.user.id, oldPassword, newPassword); | ||
|
|
||
| res.sendStatus(204); |
There was a problem hiding this comment.
The changePassword handler validates the new password and confirmation but does not validate that oldPassword was provided. The task requires that changing password must require an old one. Consider checking for presence of oldPassword and returning a validation error (e.g. { oldPassword: 'Password is required' }) before calling the service.
| async function changeEmail(req, res) { | ||
| const { password, email } = req.body; | ||
|
|
||
| if (!email) { | ||
| throw ApiError.BadRequest('Validation error', { | ||
| email: 'Email is required', | ||
| }); | ||
| } | ||
|
|
||
| if (!password) { | ||
| throw ApiError.BadRequest('Validation error', { | ||
| password: 'Password is required', | ||
| }); | ||
| } | ||
|
|
||
| const user = await userService.changeEmail(req.user.id, password, email); | ||
|
|
||
| res.send(user); |
There was a problem hiding this comment.
The changeEmail handler checks presence of password and email, then calls userService.changeEmail and returns the updated user. The task requires that email change should require confirmation of the new email and notify the old email about the change. Returning the user with the new email immediately (before confirmation) violates that requirement. Either adjust the service so the email isn't replaced until the new email is confirmed and send a notification to the old email, or change this controller to respond with an informative message (e.g. 'We sent a confirmation link to the new email') instead of the updated user object.
| await userService.register({ name, email, password }); | ||
|
|
||
| res.send({ message: 'OK' }); | ||
| } | ||
|
|
||
| async function activate(req, res, next) { | ||
| const { activationToken } = req.params; | ||
|
|
||
| const user = await User.findOne({ | ||
| where: { activationToken }, | ||
| }); | ||
|
|
||
| if (!user) { | ||
| res.sendStatus(404); | ||
| return; | ||
| } | ||
|
|
||
| user.activationToken = null; |
There was a problem hiding this comment.
The changeEmail handler only requires password and email, but the task requires confirming the new email and notifying the old email about the change. Add a confirmation (or emailConfirmation) field and validate it matches email. Also ensure the old email gets notified (either here or confirm userService implements and documents that behavior).
| await userService.register({ name, email, password }); | ||
|
|
||
| res.send({ message: 'OK' }); | ||
| } | ||
|
|
||
| async function activate(req, res, next) { | ||
| const { activationToken } = req.params; |
There was a problem hiding this comment.
There is no validation that email has a proper format before calling the service. Consider validating email format (like other controllers do) and returning a structured validation error to match the app's error handling.
| if (!value) { | ||
| return 'Name is required'; | ||
| } | ||
| } | ||
|
|
||
| async function register(req, res, next) { | ||
| const { name, email, password } = req.body; | ||
|
|
||
| const errors = { | ||
| name: validateName(name), | ||
| email: validateEmail(email), | ||
| password: validatePassword(password), | ||
| }; | ||
|
|
||
| if (errors.name || errors.email || errors.password) { | ||
| throw ApiError.BadRequest('Validation error', errors); |
There was a problem hiding this comment.
The changePassword handler does not validate presence of oldPassword before calling the service. The requirement states the old password must be provided — add an explicit validation that oldPassword is present and return a validation error if missing (so clients get a clear structured response).
| useEffect(() => { | ||
| checkAuth(); | ||
| }, []); |
There was a problem hiding this comment.
The effect calls checkAuth() but has an empty dependency array. React will warn that checkAuth should be included. Either add checkAuth to the deps or ensure checkAuth is stable (e.g. memoized in the AuthContext) to avoid linter warnings and potential stale-closure issues.
| import './styles.scss'; | ||
|
|
||
| import { AccountActivationPage } from './pages/AccountActivationPage'; | ||
| import { AuthContext } from './components/AuthContext'; |
There was a problem hiding this comment.
Imports are inconsistent: some components are imported with the .jsx extension and some without (e.g. AuthContext is imported without an extension). While this usually works with Vite, prefer consistent import style (include or omit extensions project-wide) to avoid resolution surprises.
| useEffect(() => { | ||
| activate(activationToken) | ||
| .then(() => { | ||
| navigate('/profile'); | ||
| }) | ||
| .catch(error => { | ||
| setError(error.response?.data?.message || `Wrong activation link`); | ||
| }) | ||
| .finally(() => { | ||
| setDone(true); | ||
| }); | ||
| }, []); |
There was a problem hiding this comment.
The effect calls activate(activationToken) and uses navigate but the dependency array is empty. This can trigger React Hook linter warnings and could cause stale-closure issues. Include dependencies such as [activate, activationToken, navigate] (or explicitly document/disable the lint rule) or ensure activate is stable (e.g. memoized) in the context.
| root.render( | ||
| <AuthProvider> | ||
| <Router> | ||
| <App /> |
There was a problem hiding this comment.
You call useParams() and read activationToken but there is no guard if it's undefined. Consider checking if (!activationToken) { setError('Missing activation token'); setDone(true); return; } before calling activate so users see a clear error instead of the request failing ambiguously.
| } | ||
|
|
||
| export const PasswordResetPage = () => { | ||
| const { passwordResetToken } = useParams(); |
There was a problem hiding this comment.
You read passwordResetToken from the route params but do not validate its presence. If the token is missing or undefined the request will target /password-reset/undefined. Add a guard to show a clear error to the user (or disable the form) when passwordResetToken is absent so the UI doesn't attempt a meaningless network call.
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Review Summary
I am requesting changes because, although most auth flows are implemented correctly, there are still several critical issues that break required functionality and violate the checklist.
- Backend routes that must be available only to non‑authenticated users (e.g.
POST /register,GET /activate/:activationToken,POST /login,POST /password-reset,POST /password-reset/:passwordResetTokeninbackend/src/routes/authRouter.jsand their handlers inauthController.js/userService.js) do not check whetherreq.useris already set; you should add a guard/middleware that rejects these endpoints when an access token is valid, as the requirements explicitly demand they be usable only by non‑authenticated users. - Logout is fragile on both sides: on the backend, the logout handler only removes the stored refresh token when the refresh cookie is present, even though
authMiddlewarehas already populatedreq.user; you should usereq.user.idas a reliable fallback to remove the refresh token from DB so the session is always invalidated, even if the cookie is missing. On the frontend (seefrontend/src/services/authService.jsand its use inAuthContext.jsx),logout()is sent via the unauthenticated client (authClient, without Authorization header) while/logoutis protected byauthMiddleware, so the request will 401 and the user will not be properly logged out; switch this call tohttpClient(or otherwise include the access token) so logout actually succeeds. - The email‑change flow is currently broken: the frontend
changeEmailcalls (infrontend/src/services/userService.js/authService.jsand the forms that use them, e.g.ProfilePage.jsx) send only{ password, email }, but the backenduserController.changeEmailrequires anemailConfirmationfield and validates that it matches; update the UI to collectemailConfirmationand send{ password, email, emailConfirmation }so this checklist feature works end‑to‑end. - Additionally, activation and password‑reset handlers on the backend (e.g. actions using
req.params.activationTokenandreq.params.passwordResetToken) should explicitly validate that these tokens are present and return a clearApiError.BadRequestwhen missing rather than relying on downstream 404/undefined behavior, as the requirements call for clear validation errors for missing tokens.
Once you address these blockers, your solution will align well with the task requirements; the remaining suggestions (e.g. more robust Bearer parsing in authMiddleware, casting SMTP_PORT to a number, using a real from email, and improving isChecked/hook dependencies in AuthContext.jsx) are minor polish that you can refine after fixing the core issues.
✨ 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
|
|
||
| this.status = status; | ||
| this.errors = errors; | ||
| } | ||
|
|
||
| static BadRequest(message, errors) { |
There was a problem hiding this comment.
Consider calling next(ApiError.Unauthorized()) instead of throwing here. Throwing a synchronous error often works, but calling next(err) makes the error flow explicit and avoids surprises if this middleware is used in contexts where thrown sync errors aren't forwarded to the error handler.
| } | ||
|
|
||
| static Unauthorized() { | ||
| return new ApiError(401, 'User is not authorized'); | ||
| } |
There was a problem hiding this comment.
Parsing the header with authHeader.split(' ') works for the usual Bearer <token> format, but extra spaces could cause unexpected results. Consider const [, accessToken] = authHeader.split(' ', 2) or verifying authHeader.startsWith('Bearer ') and using authHeader.slice(7).trim() to be more robust.
|
|
||
| const transporter = nodemailer.createTransport({ | ||
| host: process.env.SMTP_HOST, | ||
| port: process.env.SMTP_PORT, |
There was a problem hiding this comment.
SMTP port is read from process.env.SMTP_PORT and passed directly to nodemailer. Consider casting it to a number (Number(process.env.SMTP_PORT)) to avoid unexpected type issues on some platforms/configs.
| const transporter = nodemailer.createTransport({ | ||
| host: process.env.SMTP_HOST, | ||
| port: process.env.SMTP_PORT, | ||
| secure: false, // true for 465, false for other ports |
There was a problem hiding this comment.
The secure flag is hardcoded to false. For better security and compatibility, enable secure: true when using port 465 or when running in production (e.g. secure: Number(process.env.SMTP_PORT) === 465 || process.env.NODE_ENV === 'production').
|
|
||
| export function send({ email, subject, html }) { | ||
| return transporter.sendMail({ | ||
| from: 'Auth API', // sender address |
There was a problem hiding this comment.
The from field in send() is set to the string 'Auth API'. Many SMTP providers require a valid email address here; consider using a real sender address (e.g. process.env.SMTP_FROM || process.env.SMTP_USER) to avoid delivery/rejection issues.
| import { authController } from '../controllers/authController.js'; | ||
| import { authMiddleware } from '../middlewares/authMiddleware.js'; | ||
| import { catchError } from '../middlewares/catchError.js'; |
There was a problem hiding this comment.
Consider converting process.env.SMTP_PORT to a number before passing to nodemailer (e.g. port: Number(process.env.SMTP_PORT)) — env vars are strings and explicit casting avoids unexpected behavior in some environments.
| ); | ||
| authRouter.post('/login', catchError(authController.login)); | ||
| authRouter.post('/logout', authMiddleware, catchError(authController.logout)); | ||
| authRouter.get('/refresh', catchError(authController.refresh)); | ||
| authRouter.post('/password-reset', catchError(authController.requestPasswordReset)); |
There was a problem hiding this comment.
Using from: 'Auth API' is valid, but it's better to provide a full sender address (e.g. from: 'Auth API <no-reply@example.com>') so receiving mail servers and inboxes can display and filter messages reliably.
| import './models/User.js'; | ||
| import './models/Token.js'; | ||
|
|
||
| sequelize.sync({ force: true }); |
There was a problem hiding this comment.
The call sequelize.sync({ force: true }) returns a promise but is not awaited or handled. If this script is executed directly, the process may exit before the sync finishes or errors may be lost. Wrap the call in an async IIFE or use .then().catch() to handle success/errors (and log them) so failures are visible in CI/dev runs.
| import './models/User.js'; | ||
| import './models/Token.js'; | ||
|
|
||
| sequelize.sync({ force: true }); |
There was a problem hiding this comment.
force: true drops and recreates all tables which will delete production data. Add a clear comment or gate this behind a NODE_ENV === 'development' check (or an env flag) to prevent accidental data loss when the script runs in non-dev environments.
| import { User } from '../models/User.js'; | ||
|
|
||
| function getAllActive() { | ||
| return User.findAll({ |
There was a problem hiding this comment.
Registration is currently exposed server-side without checking whether the caller is already authenticated. The checklist requires registration to be available only to non-authenticated users — add a middleware or an early check (e.g. if (req.user) throw ApiError.BadRequest('Already authenticated')) before proceeding.
| where: { activationToken: null }, | ||
| order: ['id'], | ||
| }); |
There was a problem hiding this comment.
Activation route is callable by any client. Per requirements the Activation page/endpoint should be available only to non-authenticated users — add a guard (middleware or controller-level check) to reject requests when req.user is present.
| order: ['id'], | ||
| }); | ||
| } | ||
|
|
There was a problem hiding this comment.
Login route is not protected against already-authenticated users. The task requires login to be available only to non-authenticated users — add a guard (middleware or controller-level check). Also consider validating presence of email and password and returning structured validation errors before calling the service.
| where: { email }, | ||
| }); | ||
| } | ||
|
|
There was a problem hiding this comment.
Password-reset request and reset routes are currently callable by authenticated users. The checklist requires password reset flows to be "only non authenticated" — add guards to both /password-reset and /password-reset/:passwordResetToken routes (or validate req.user in their controllers) to reject authenticated callers.
|
|
||
| const confirmationToken = uuidv4(); | ||
| const oldEmail = user.email; | ||
|
|
||
| user.newEmail = newEmail; | ||
| user.activationToken = confirmationToken; | ||
| await user.save(); | ||
|
|
||
| await emailService.sendEmailChangeConfirmation(newEmail, confirmationToken); |
There was a problem hiding this comment.
Logout removes the stored refresh token only when a valid refresh cookie is present. Because the logout route is protected by access-token middleware, req.user is available and should be used as a reliable fallback to remove the server-side refresh token (e.g. await tokenService.remove(req.user.id)), ensuring the session is invalidated even if the cookie is missing.
| throw ApiError.BadRequest('User with this email does not exist'); | ||
| } |
There was a problem hiding this comment.
Activation handler reads activationToken from params but does not validate its presence explicitly. Add an upfront check (e.g. if !activationToken throw ApiError.BadRequest with a structured error) so clients receive a clear validation error when the token is missing.
| function generateAccessToken(user) { | ||
| return jwt.sign(user, process.env.JWT_ACCESS_SECRET, { expiresIn: '15m' }); | ||
| } | ||
|
|
There was a problem hiding this comment.
sequelize.sync({ force: true }) will drop and recreate all tables — this is destructive and will erase production data. Restrict force: true to development/test environments (for example, only when NODE_ENV !== 'production' or controlled by an env var) or prefer migrations for schema changes.
| function generateAccessToken(user) { | ||
| return jwt.sign(user, process.env.JWT_ACCESS_SECRET, { expiresIn: '15m' }); | ||
| } | ||
|
|
There was a problem hiding this comment.
The script calls sequelize.sync(...) without handling errors or awaiting completion. Wrap the call in an async function and add .catch(...) (or try/catch) so failures are logged and the process exits with a non-zero code. This makes CI/automation failures visible and avoids silent errors.
|
|
||
| export const AuthProvider = ({ children }) => { | ||
| const [user, setUser] = useState(null); | ||
| const [isChecked, setChecked] = useState(true); |
There was a problem hiding this comment.
isChecked should start as false. Currently it's initialized with useState(true) which means consumers (e.g. App) won't show the loading state while checkAuth() runs on mount. Change to useState(false) so the app can wait for the auth check (and set it to true in checkAuth finally).
| const value = useMemo( | ||
| () => ({ | ||
| isChecked, | ||
| user, | ||
| checkAuth, | ||
| activate, | ||
| login, | ||
| logout, | ||
| }), | ||
| [user, isChecked], | ||
| ); |
There was a problem hiding this comment.
The useMemo dependency array includes only [user, isChecked] but the memoized value contains function references (checkAuth, activate, login, logout). Either wrap those functions with useCallback or add them to the dependency list to avoid stale closures and React hook warnings.
| <title>Login App</title> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> |
There was a problem hiding this comment.
isChecked is initialized to true. That prevents the app from showing the loading state while checkAuth() runs on mount — set the initial value to false (so the UI shows a loader until checkAuth() finishes and sets it to true).
|
|
||
| import { AccountActivationPage } from './pages/AccountActivationPage'; | ||
| import { AuthContext } from './components/AuthContext'; | ||
| import { LoginPage } from './pages/LoginPage'; |
There was a problem hiding this comment.
isChecked is initialized to true. Because App relies on isChecked to show a loader while checkAuth() runs, initialize this to false so the UI does not incorrectly consider the auth check complete before checkAuth finishes.
| </NavLink> | ||
|
|
||
| <NavLink to="/users" className="navbar-item"> | ||
| Users | ||
| </NavLink> | ||
|
|
||
| {user && ( | ||
| <NavLink to="/profile" className="navbar-item"> | ||
| Profile | ||
| </NavLink> |
There was a problem hiding this comment.
The useMemo dependency array includes only user and isChecked, but the memoized value also closes over the functions checkAuth, activate, login, and logout. To avoid stale closures and satisfy React hooks lint rules, either wrap those functions with useCallback and include them in the deps, or include the functions themselves in the dependency array.
| const [error, setError] = usePageError(); | ||
| const { isChecked, user, logout, checkAuth } = useContext(AuthContext); |
There was a problem hiding this comment.
Minor: typo in the catch block log message ('User is not authentincated'). Correcting to 'User is not authenticated' will make logs clearer.
| export const usePageError = initialError => { | ||
| const [error, setError] = useState(initialError); |
There was a problem hiding this comment.
Consider giving initialError a default value (for example '' or null) in the parameter list so callers that omit the argument get a predictable initial state and to avoid undefined typed values in state.
| const timerId = setTimeout(() => { | ||
| setError(''); |
There was a problem hiding this comment.
You currently reset the error to an empty string (setError('')). If the rest of the app treats null/undefined and empty string differently, prefer resetting to the same type as initialError (e.g. setError(initialError ? '' : initialError) or use null consistently). This keeps the error state type-consistent.
| import { AuthContext } from './AuthContext.jsx'; | ||
| import { Loader } from './Loader.jsx'; |
There was a problem hiding this comment.
Consider providing a default for initialError (for example initialError = '') so the hook always initializes error to a predictable string instead of undefined when callers omit the argument. This avoids the if (!error) return; early return being triggered for undefined vs an empty string and makes the hook's contract clearer.
| function refresh() { | ||
| return authClient.get('/refresh'); |
There was a problem hiding this comment.
The backend userController.changeEmail validates an emailConfirmation field (and the task requires confirming the new email). This changeEmail function sends only { password, email }, so the server will return a validation error (Email confirmation is required). Update this function (and the UI where it's called) to include and send emailConfirmation (e.g. accept a third parameter or an object with emailConfirmation) so the frontend matches the backend and the task requirement.
| <div className="content"> | ||
| <h1 className="title">Users</h1> |
There was a problem hiding this comment.
The changeEmail function only sends { password, email } but the backend userController.changeEmail expects password, email and emailConfirmation and validates that the two emails match. Update this function to accept and send emailConfirmation in the request body (for example: changeEmail(password, email, emailConfirmation) { return httpClient.patch('/users/email', { password, email, emailConfirmation }); }) and update any callers (ProfilePage) to provide the confirmation value.
| function changeEmail(password, email) { | ||
| return httpClient.patch('/users/email', { password, email }); | ||
| } |
There was a problem hiding this comment.
Mismatch with backend validation: backend userController.changeEmail expects password, email and emailConfirmation. This client function only sends { password, email }, so the server will return a validation error. Change the function signature to accept and send emailConfirmation (e.g. changeEmail(password, email, emailConfirmation) { return httpClient.patch('/users/email', { password, email, emailConfirmation }); }) and update the Profile page to provide the confirmation value.
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Review Summary
I am approving your solution as an educational override so you can continue progressing while you address a few remaining issues, because overall you’ve implemented the full auth and profile flows very well (registration/activation, login/logout, password reset, and profile changes all line up with the expected behavior). The main critical gap is that in backend/src/controllers/authController.js the guest-only handlers (register, activate, login, requestPasswordReset, resetPassword) should explicitly reject authenticated users at the top of each function (around where you first read req.body), e.g. if (req.user) throw ApiError.BadRequest('Already authenticated'); to fully satisfy the “guest-only endpoints” requirement. Another important robustness fix is to add null checks after database lookups in backend/src/services/userService.js and backend/src/services/emailService.js (right after User.findByPk(...) / User.findOne(...)) so you return a clear ApiError instead of crashing if a user record is missing. For authentication robustness, please make the Authorization header parsing in backend/src/middlewares/authMiddleware.js, backend/src/middlewares/guestMiddleware.js, and the token lookup in backend/src/models/Token.js more tolerant (trim the header, split once, and compare bearer case-insensitively) so valid tokens aren’t rejected due to spacing or casing. On the frontend, watch out for the circular dependency between frontend/src/http/httpClient.js and frontend/src/services/authService.js, and make logout in frontend/src/components/AuthContext.jsx always clear local auth state in a finally block so a network failure can’t leave the UI logged in. As a minor but practical improvement, avoid hard-coded recipient emails and auto-running test scripts in backend/src/routes/authRouter.js, backend/src/send-mail.js, and backend/src/routes/userRouter.js by moving addresses to env vars and guarding execution with if (require.main === module). Overall, your structure, validation logic, and alignment between frontend and backend are strong, and with these refinements you’ll have a very robust and production-ready implementation.
✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨
Footnotes
| } | ||
| } | ||
|
|
||
| async function register(req, res, next) { |
There was a problem hiding this comment.
register handler must be available only to non‑authenticated users (checklist). Add an explicit guard at the start of the function to reject when req.user is set (e.g. throw ApiError.BadRequest('Already authenticated')). Relying only on external middleware may be insufficient — the checklist requires these backend endpoints to reject authenticated requests.
| res.send({ message: 'OK' }); | ||
| } | ||
|
|
||
| async function activate(req, res, next) { |
There was a problem hiding this comment.
activate handler should also reject authenticated users (activation page is only for non‑authenticated). Add a check such as if (req.user) throw ApiError.BadRequest('Already authenticated') before processing the activation token.
| await sendAuthentication(res, user); | ||
| } | ||
|
|
||
| async function login(req, res, next) { |
There was a problem hiding this comment.
login handler must be accessible only to non‑authenticated users. Add an explicit guard at the start to reject when req.user exists to satisfy the checklist requirement.
| }); | ||
| } | ||
|
|
||
| async function requestPasswordReset(req, res) { |
There was a problem hiding this comment.
requestPasswordReset handler is required to be available only to non‑authenticated users. Add a guard checking req.user and return a clear ApiError.BadRequest if already authenticated.
| res.send({ message: 'Check your email for the reset link' }); | ||
| } | ||
|
|
||
| async function resetPassword(req, res) { |
There was a problem hiding this comment.
resetPassword handler (uses passwordResetToken) must also be available only to non‑authenticated users. Although token presence is validated, add a check to reject when req.user is set to meet the checklist rules.
|
|
||
| static BadRequest(message, errors) { | ||
| return new ApiError(400, message, errors); | ||
| } |
There was a problem hiding this comment.
Token extraction could be more robust: trim the header, parse into scheme and the rest (const [scheme, ...rest] = authHeader.trim().split(/\s+/); const accessToken = rest.join(' ');) and compare scheme case‑insensitively (scheme.toLowerCase() !== 'bearer'). This avoids false negatives for authorization: bearer ... or extra spaces.
| if (!emailPattern.test(value)) { | ||
| return 'Email is not valid'; | ||
| } |
There was a problem hiding this comment.
Small robustness improvement: make the scheme check case-insensitive and limit splitting to two parts to avoid unexpected results when the header contains extra spaces. Example: const [scheme, accessToken] = authHeader.split(' ', 2); if (scheme?.toLowerCase() !== 'bearer' || !accessToken) { throw ApiError.Unauthorized(); }
| import { jwtService } from '../services/jwtService.js'; | ||
|
|
||
| export function authMiddleware(req, res, next) { | ||
| const authHeader = req.headers['authorization']; |
There was a problem hiding this comment.
Trim the header and guard against undefined to avoid errors when a malformed/empty header is present, e.g. const authHeader = req.headers['authorization']?.trim();. This makes the subsequent parsing more robust.
| const [scheme, accessToken] = authHeader.split(' '); | ||
|
|
||
| if (scheme !== 'Bearer' || !accessToken) { |
There was a problem hiding this comment.
Be more tolerant when parsing the scheme/token. Right now authHeader.split(' ') + strict scheme !== 'Bearer' is case-sensitive and sensitive to multiple spaces. Consider parsing like const [scheme, ...rest] = authHeader.split(/\s+/); const accessToken = rest.join(' '); and compare scheme.toLowerCase() !== 'bearer' so Bearer, bearer, or extra whitespace won’t incorrectly reject a valid token.
| return async (req, res, next) => { | ||
| try { | ||
| await action(req, res, next); | ||
| } catch (error) { |
There was a problem hiding this comment.
Be defensive when reading the header to avoid errors and extra whitespace: consider const authHeader = req.headers['authorization']?.trim(); so a missing header or leading/trailing spaces don't cause unexpected behavior.
| import { DataTypes } from 'sequelize'; | ||
| import { sequelize } from '../utils/db.js'; | ||
| import { User } from './User.js'; | ||
|
|
There was a problem hiding this comment.
Make guestMiddleware defensive to cover cases where earlier middleware already populated req.user. Add an early check like if (req.user) throw ApiError.BadRequest('Already authenticated'); so guest-only endpoints are rejected when the request is already authenticated.
| }, | ||
| }); | ||
|
|
||
| Token.belongsTo(User); |
There was a problem hiding this comment.
Parsing the Authorization header here only extracts the second space-separated part ([, accessToken] = authHeader.split(' ')). Consider validating the scheme and being tolerant to whitespace/case: e.g. trim the header, split by whitespace, check scheme.toLowerCase() === 'bearer' and then use the token. This avoids false negatives for different header formats.
| Token.belongsTo(User); | ||
| User.hasOne(Token); |
There was a problem hiding this comment.
Token associations (Token.belongsTo(User) / User.hasOne(Token)) are fine. If you expect tokens to be removed when a user is deleted, consider adding onDelete: 'CASCADE'/onUpdate options in the association or when defining the foreign key to make lifecycle explicit.
| import { authMiddleware } from '../middlewares/authMiddleware.js'; | ||
| import { guestMiddleware } from '../middlewares/guestMiddleware.js'; | ||
| import { catchError } from '../middlewares/catchError.js'; | ||
|
|
||
| export const authRouter = new express.Router(); |
There was a problem hiding this comment.
This file calls send(...) with a hardcoded recipient address. Avoid committing real email addresses — either load the recipient from an environment variable (e.g. process.env.TEST_EMAIL) or remove the script from the repository to prevent accidental emails.
| import express from 'express'; | ||
|
|
||
| import { authController } from '../controllers/authController.js'; | ||
| import { authMiddleware } from '../middlewares/authMiddleware.js'; |
There was a problem hiding this comment.
The script executes send() immediately on import. That can trigger emails in CI or when the module is imported elsewhere. Wrap the action in a guard (e.g. if (require.main === module) or check process.env.NODE_ENV) or convert to a small CLI so it only runs intentionally.
| send({ | ||
| email: 'sewihi7454@nifect.com', |
There was a problem hiding this comment.
Avoid hardcoding the recipient address. Use an environment variable or a CLI argument so you don't accidentally commit real or test addresses in the repo. Example: email: process.env.TEST_MAIL_RECIPIENT.
| send({ | ||
| email: 'sewihi7454@nifect.com', | ||
| subject: 'Test', | ||
| html: '123', | ||
| }); |
There was a problem hiding this comment.
The send function returns a Promise but its result/error is not handled. Add await inside an async IIFE or use .then().catch() to surface errors and avoid unhandled promise rejections during execution.
| import 'dotenv/config'; | ||
| import { send } from './services/emailService.js'; | ||
|
|
||
| send({ | ||
| email: 'sewihi7454@nifect.com', | ||
| subject: 'Test', | ||
| html: '123', | ||
| }); |
There was a problem hiding this comment.
This script executes immediately when imported/run. Keep it as a development helper only (or protect it with a check like if (require.main === module)) and do not run it in production/CI. Consider removing it before deployment or documenting its intended usage in README.
| import { catchError } from '../middlewares/catchError.js'; | ||
|
|
||
| export const userRouter = new express.Router(); | ||
|
|
There was a problem hiding this comment.
This script contains a hard-coded test email address and will send an email immediately when run. Consider removing this file from the repository, guarding execution (so it doesn't run on app start), or reading the recipient/from/subject from environment variables for safer testing.
| sendActivationLink, | ||
| sendPasswordResetLink, | ||
| sendEmailChangeConfirmation, | ||
| sendEmailChangeNotification, | ||
| }; |
There was a problem hiding this comment.
After User.findByPk(userId) you assume a user always exists. If the user record was removed between authentication and this request, user will be null and the next lines will throw. Add a null check and return a clear ApiError (e.g. NotFound or Unauthorized) before using user.
| async function changeName(userId, name) { | ||
| const user = await User.findByPk(userId); | ||
|
|
||
| user.name = name; | ||
| await user.save(); | ||
|
|
||
| return normalize(user); |
There was a problem hiding this comment.
changeName assumes the user exists after findByPk. If user is null this will throw when attempting to set user.name. Add an explicit existence check (e.g. throw ApiError.NotFound() or ApiError.Unauthorized() depending on desired semantics) before modifying and saving the user.
| async function changePassword(userId, oldPassword, newPassword) { | ||
| const user = await User.findByPk(userId); | ||
| const isValid = await bcrypt.compare(oldPassword, user.password); | ||
|
|
||
| if (!isValid) { | ||
| throw ApiError.BadRequest('Validation error', { | ||
| oldPassword: 'Wrong password', | ||
| }); | ||
| } |
There was a problem hiding this comment.
changePassword calls bcrypt.compare(oldPassword, user.password) but does not check that user was found. If findByPk returns null this will cause an exception. Add a check after the lookup to throw a clear ApiError when the user is not found.
| async function changeEmail(userId, password, newEmail) { | ||
| const user = await User.findByPk(userId); | ||
| const isValid = await bcrypt.compare(password, user.password); | ||
|
|
||
| if (!isValid) { | ||
| throw ApiError.BadRequest('Validation error', { | ||
| password: 'Wrong password', | ||
| }); | ||
| } |
There was a problem hiding this comment.
changeEmail also assumes the user exists before calling bcrypt.compare. Add a defensive check after findByPk to return a clear error if the user record is missing. This will avoid uncaught exceptions and make the API behavior consistent.
|
|
||
| function App() { | ||
| const navigate = useNavigate(); | ||
| const [error, setError] = usePageError(); |
There was a problem hiding this comment.
usePageError is called without an initial value here while other pages pass an initial string (e.g. ''). For consistency and to avoid an undefined error state, consider calling usePageError('') so error state starts as an empty string.
| useEffect(() => { | ||
| checkAuth(); | ||
| }, []); |
There was a problem hiding this comment.
The effect calls checkAuth() but the dependency array is empty. Although checkAuth is a stable callback in AuthContext, add checkAuth to the dependency array to satisfy React rules and avoid future issues if checkAuth implementation changes.
| const logout = useCallback(async () => { | ||
| await authService.logout(); | ||
|
|
||
| accessTokenService.remove(); | ||
| setUser(null); |
There was a problem hiding this comment.
Make client logout resilient: if authService.logout() fails (network/server error) the function currently won't clear the local access token or user state. To guarantee the UI is logged out even when the server call fails, move accessTokenService.remove() and setUser(null) into a finally block (or clear them in a catch and rethrow if needed). This prevents stale client state when server logout cannot be completed.
| const activate = useCallback(async activationToken => { | ||
| const { accessToken, user: u } = | ||
| await authService.activate(activationToken); | ||
|
|
||
| accessTokenService.save(accessToken); | ||
| setUser(u); | ||
| }, []); | ||
|
|
||
| const checkAuth = useCallback(async () => { | ||
| try { | ||
| const { accessToken, user: u } = await authService.refresh(); | ||
|
|
||
| accessTokenService.save(accessToken); | ||
| setUser(u); | ||
| } catch { | ||
| // not authenticated | ||
| } finally { | ||
| setChecked(true); | ||
| } | ||
| }, []); | ||
|
|
||
| const login = useCallback(async ({ email, password }) => { | ||
| const { accessToken, user: u } = await authService.login({ | ||
| email, | ||
| password, | ||
| }); | ||
|
|
||
| accessTokenService.save(accessToken); | ||
| setUser(u); | ||
| }, []); |
There was a problem hiding this comment.
Small lint/clarity improvement: your callbacks (activate, checkAuth, login, logout) use imported services (authService, accessTokenService) that are module-level singletons and thus stable. Still, to avoid React Hooks linter warnings in other codebases, consider explicitly listing external dependencies in the useCallback dependency arrays (or document why empty arrays are safe). Alternatively you can keep them as-is since the services are stable.
| } | ||
|
|
||
| return ( |
There was a problem hiding this comment.
This component assumes user is always present (accessing user.name / user.email). The route is protected by RequireAuth so this is normally safe, but for extra robustness add a defensive check (e.g. if (!user) return null;) or a small guard to avoid runtime errors in case the component is mounted outside that context during tests or refactors.
| const key = 'accessToken'; | ||
|
|
There was a problem hiding this comment.
Potential circular dependency: httpClient imports authService and authService imports httpClient (used for logout). This can lead to authService being undefined when httpClient is initialized in some environments. To avoid this, either: 1) move the refresh call out of authService into a small module used by both (e.g. tokenApi.refresh()), 2) have httpClient call authClient.get('/refresh') directly instead of authService.refresh(), or 3) perform a dynamic import inside onResponseError (e.g. const { authService } = await import('../services/authService.js')) so the reference is resolved at call time.
No description provided.