Conversation
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Review Summary
I am requesting changes because there are two critical requirement violations that affect core functionality and access control. First, your backend routes for POST /register and POST /login in src/backend/src/routes/auth.routes.js are not protected by guestOnlyMiddleware, even though the specification requires registration and login to be available only to non‑authenticated users; this means authenticated users can still call these endpoints, which breaks that requirement. Second, there is no server-side logout endpoint at all, while the task explicitly requires a logout action that is only possible for authenticated users and results in redirecting back to the login page; you should add a route like POST /logout protected by authMiddleware that destroys any server session state (e.g., req.session.destroy(...)) and lets the frontend redirect to /login.
Aside from these, your implementation is strong: registration, activation, login checks for activation, password reset flows, profile updates, email-change confirmation/notification, and GitHub OAuth are all well structured and align with the task. As smaller improvements (not blockers), consider adding a guard in saveSession (in auth.controller.js) to handle missing req.session, validating required JWT and mail environment variables at startup, and optionally providing a dev/test mail transport so email flows don’t crash when SMTP credentials aren’t configured. Once you add guest-only protection to register/login and implement the protected logout endpoint, your solution will meet the critical checklist items and should be ready to pass.
✨ 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 register(req, res, next) { | ||
| try { | ||
| const { name, email, password } = req.body; | ||
|
|
||
| await authService.register({ name, email, password }); | ||
|
|
||
| res.status(201).json({ |
There was a problem hiding this comment.
The auth controller's methods assume guest-only vs authenticated access is enforced by route middlewares, but in auth.routes.js I see /register and /login are not using guestOnlyMiddleware here. Ensure these routes are protected so only non-authenticated users can call them, as required by the task: "Register... only non‑authenticated" and "Login... only non‑authenticated".
| async function activate(req, res, next) { | ||
| try { | ||
| const { hash } = req.params; | ||
| const user = await authService.activate(hash); | ||
|
|
||
| const token = jwtService.sign({ id: user.id, email: user.email }); | ||
| const clientUrl = process.env.CLIENT_URL || 'http://localhost:5173'; | ||
| const search = new URLSearchParams({ | ||
| accessToken: token, | ||
| flow: 'registration', | ||
| }); | ||
|
|
||
| res.redirect(`${clientUrl}/activation?${search.toString()}`); |
There was a problem hiding this comment.
activate redirects client to the frontend activation page; requirement: "The application must provide an activation page that is accessible only for non‑authenticated users." Ensure the frontend activation page enforces guest-only access (client-side hook exists) and that server-side route is accessible without auth — this code is fine but confirm frontend protects it. If activation page must be server-protected for guests only, add guestOnlyMiddleware to this route.
| async function login(req, res, next) { | ||
| try { | ||
| const { email = '', password = '' } = req.body; | ||
| const result = await authService.login({ email, password }); | ||
|
|
||
| res.json(result); |
There was a problem hiding this comment.
login returns a JWT but does not set any session state; this is fine if client stores token. Requirement: "If a user attempts to log in and is not active, the application must ask them to activate their email." The authService.login already checks activationToken and throws unauthorized with message about activation, so controller will propagate this — good. Just ensure client displays that message.
| async function requestPasswordReset(req, res, next) { | ||
| try { | ||
| const { email = '' } = req.body; | ||
|
|
||
| await authService.requestPasswordReset({ email }); | ||
|
|
||
| res.json({ | ||
| message: 'If an account exists for that email, we sent a reset link.', | ||
| }); | ||
| } catch (error) { | ||
| next(error); | ||
| } | ||
| } | ||
|
|
||
| async function resetPassword(req, res, next) { | ||
| try { | ||
| const { token = '', password = '' } = req.body; | ||
|
|
||
| await authService.resetPassword({ token, password }); | ||
|
|
||
| res.json({ message: 'Password reset successfully.' }); |
There was a problem hiding this comment.
requestPasswordReset and resetPassword are implemented and return appropriate messages. Ensure frontend password reset flow includes the Reset Password confirmation page with password and confirmation fields that must be equal — backend resetPassword only accepts token and password; front-end enforces confirmation equality, which meets requirement. No server changes required here.
| async function confirmEmailChange(req, res, next) { | ||
| try { | ||
| const { token = '' } = req.params; | ||
| const user = await userService.confirmEmailChange(token); | ||
| const accessToken = jwtService.sign({ id: user.id, email: user.email }); | ||
| const clientUrl = process.env.CLIENT_URL || 'http://localhost:5173'; | ||
| const search = new URLSearchParams({ | ||
| accessToken, | ||
| flow: 'email-change', | ||
| }); | ||
|
|
||
| res.redirect(`${clientUrl}/activation?${search.toString()}`); |
There was a problem hiding this comment.
confirmEmailChange verifies token and redirects to client activation with flow=email-change. Requirement: "To change an email, the user should type the password, confirm the new email and notify the old email about the change." The user.service.requestEmailChange and confirmEmailChange handle this (sending confirmation and notification). Controller simply redirects after confirmation — good.
| async function startGithubAuthentication(req, res, next) { | ||
| try { | ||
| if (!isGithubAuthEnabled()) { | ||
| throw ApiError.badRequest('GitHub sign-in is not configured.'); | ||
| } | ||
|
|
||
| req.session.githubAuth = { | ||
| intent: 'authenticate', | ||
| }; | ||
|
|
||
| await saveSession(req); | ||
|
|
||
| const serverUrl = process.env.SERVER_URL || 'http://localhost:4000'; | ||
|
|
||
| res.json({ | ||
| url: `${serverUrl}/auth/github`, | ||
| }); |
There was a problem hiding this comment.
GitHub OAuth flow saves state in express-session (req.session.githubAuth). app.js configures express-session and passport.initialize(), but I don't see passport.session() used — since authenticate is called with session:false and session data is stored manually in req.session, this is acceptable. However if you expect passport to restore session from serializeUser/deserializeUser add passport.session() and corresponding serialization. Currently the flow uses req.session and saveSession/destroySession helpers — works as implemented.
| function githubAuthenticate(req, res, next) { | ||
| if (!isGithubAuthEnabled()) { | ||
| next(ApiError.badRequest('GitHub sign-in is not configured.')); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| passport.authenticate('github', { | ||
| scope: ['user:email'], | ||
| session: false, | ||
| state: true, | ||
| })(req, res, next); |
There was a problem hiding this comment.
githubAuthenticate uses passport.authenticate with state:true. passport-github2 will expect the OAuth flow state to be handled; because you persist intent in req.session and then call /auth/github which triggers OAuth redirect, ensure session cookie is set and sent during provider redirect/callback (express-session cookie settings are present). Note cookie.secure is tied to NODE_ENV; for dev ensure secure:false so session persists across redirects.
|
|
||
| function githubCallback(req, res, next) { | ||
| if (!isGithubAuthEnabled()) { | ||
| next(ApiError.badRequest('GitHub sign-in is not configured.')); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| passport.authenticate( | ||
| 'github', | ||
| { | ||
| session: false, | ||
| }, | ||
| async (error, profile) => { | ||
| const sessionData = req.session?.githubAuth || { | ||
| intent: 'authenticate', | ||
| }; | ||
|
|
||
| const isLinkFlow = sessionData.intent === 'link'; | ||
| const fallbackRedirect = isLinkFlow ? '/account' : '/login'; | ||
|
|
||
| if (error || !profile) { | ||
| await destroySession(req); | ||
|
|
||
| res.redirect( | ||
| buildClientRedirect(fallbackRedirect, { | ||
| error: 'GitHub authentication failed. Please try again.', | ||
| }), | ||
| ); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const result = await finalizeGitHubAuthentication( | ||
| profile, | ||
| isLinkFlow ? sessionData.userId : null, | ||
| ); | ||
|
|
||
| await destroySession(req); | ||
|
|
||
| res.redirect( | ||
| buildClientRedirect(result.redirectPath, { | ||
| accessToken: result.accessToken, | ||
| notice: result.message, | ||
| }), | ||
| ); | ||
| } catch (authError) { | ||
| await destroySession(req); | ||
|
|
||
| res.redirect( | ||
| buildClientRedirect(fallbackRedirect, { | ||
| error: authError.message, | ||
| }), | ||
| ); | ||
| } | ||
| }, | ||
| )(req, res, next); |
There was a problem hiding this comment.
githubCallback handles both link and authenticate flows, finalizes via finalizeGitHubAuthentication and redirects client with accessToken in URL. This meets optional Passport.js requirement. One note: buildClientRedirect sets params only when value truthy; if accessToken is an empty string (null in link flow) it won't add — expected. No change required here.
|
|
||
| await userService.changeName({ userId: req.user.id, newName }); | ||
|
|
||
| res.json({ message: 'Name changed successfully.' }); |
There was a problem hiding this comment.
The auth controller's GitHub start endpoints set req.session.githubAuth and call saveSession which uses req.session.save. However, in app.js you only call passport.initialize() and did not call passport.session(). If your flow relies on express-session (req.session) it's fine, but be aware Passport session middleware is separate. If you intended to use passport sessions for persistent login state, add app.use(passport.session()) after session middleware in app.js.
|
|
||
| async function changeName(req, res, next) { | ||
| try { | ||
| const { newName = '' } = req.body; | ||
|
|
||
| await userService.changeName({ userId: req.user.id, newName }); | ||
|
|
||
| res.json({ message: 'Name changed successfully.' }); |
There was a problem hiding this comment.
activate and confirmEmailChange build a client redirect with an access token and redirect to the frontend activation page. This exposes an access token in the URL query string. While it works, consider returning the token in a POST response or using a short lived token (you already do) and documenting this behavior; ensure CLIENT handles storing it securely. (Not a functional bug but important security note.)
| next(error); | ||
| } | ||
| } | ||
|
|
||
| async function changePassword(req, res, next) { | ||
| try { | ||
| const { oldPassword = '', newPassword = '' } = req.body; | ||
|
|
||
| const message = await userService.changePassword({ |
There was a problem hiding this comment.
In register, login, requestPasswordReset and resetPassword handlers you accept request body values without trimming (except handled later in services). Validation is performed in service layer, which is fine, but ensure frontend and backend validation align. No change required if services enforce validation (they do).
| function authMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { | ||
| return next(ApiError.unauthorized()); | ||
| } | ||
|
|
||
| const payload = jwtService.verify(token); | ||
|
|
||
| if (!payload) { | ||
| return next(ApiError.unauthorized()); | ||
| } | ||
|
|
||
| req.user = payload; | ||
| next(); |
There was a problem hiding this comment.
authMiddleware expects an Authorization header with Bearer token. However many frontend requests use access token stored in localStorage and set Authorization header in api.request; this is consistent. Ensure any requests relying on cookie-based session authentication use appropriate middleware — currently API uses bearer JWT, not session auth.
|
|
||
| const User = sequelize.define( | ||
| 'User', | ||
| { | ||
| id: { | ||
| type: DataTypes.INTEGER, | ||
| autoIncrement: true, | ||
| primaryKey: true, |
There was a problem hiding this comment.
The errorMiddleware correctly serializes ApiError instances. Consider including stack trace in non-production environments for easier debugging, but not required by task.
| name: { | ||
| type: DataTypes.STRING, | ||
| allowNull: false, | ||
| }, |
There was a problem hiding this comment.
The middleware logs unexpected errors to stderr then sends generic 500 response. This behavior is appropriate; ensure sensitive error details are not leaked to clients (current code does not leak them).
| if (error instanceof ApiError) { | ||
| res.status(error.status).json({ | ||
| message: error.message, | ||
| errors: error.errors, | ||
| }); |
There was a problem hiding this comment.
The error middleware returns ApiError errors property even when it's empty. That's fine. No functional issues found here.
| 'use strict'; | ||
|
|
||
| const { DataTypes } = require('sequelize'); | ||
| const { sequelize } = require('../db'); | ||
| const { User } = require('./user.model'); | ||
|
|
||
| const SocialAccount = sequelize.define( | ||
| 'SocialAccount', | ||
| { | ||
| id: { | ||
| type: DataTypes.INTEGER, | ||
| autoIncrement: true, | ||
| primaryKey: true, | ||
| }, | ||
| provider: { | ||
| type: DataTypes.STRING, | ||
| allowNull: false, | ||
| }, |
There was a problem hiding this comment.
The task requires that registration and login be available only to non-authenticated users. In auth.routes.js the POST /register and POST /login routes are not protected by guestOnlyMiddleware. Add guestOnlyMiddleware to these routes so authenticated users cannot access them (e.g., authRouter.post('/register', guestOnlyMiddleware, authController.register)).
| 'use strict'; | ||
|
|
||
| const { DataTypes } = require('sequelize'); | ||
| const { sequelize } = require('../db'); | ||
| const { User } = require('./user.model'); | ||
|
|
||
| const SocialAccount = sequelize.define( | ||
| 'SocialAccount', | ||
| { | ||
| id: { | ||
| type: DataTypes.INTEGER, | ||
| autoIncrement: true, | ||
| primaryKey: true, | ||
| }, | ||
| provider: { | ||
| type: DataTypes.STRING, | ||
| allowNull: false, | ||
| }, | ||
| providerUserId: { | ||
| type: DataTypes.STRING, | ||
| allowNull: false, | ||
| field: 'providerId', | ||
| }, | ||
| providerEmail: { | ||
| type: DataTypes.STRING, | ||
| allowNull: true, | ||
| field: 'providerEmail', | ||
| }, | ||
| userId: { | ||
| type: DataTypes.INTEGER, | ||
| allowNull: false, | ||
| field: 'user_id', | ||
| }, | ||
| }, | ||
| // We use these indexes to ensure uniqueness and improve query performance | ||
| { | ||
| tableName: 'social_accounts', | ||
| indexes: [ | ||
| { | ||
| unique: true, | ||
| fields: ['provider', 'providerId'], | ||
| }, | ||
| { | ||
| unique: true, | ||
| fields: ['user_id', 'provider'], | ||
| }, | ||
| ], | ||
| }, | ||
| ); | ||
|
|
||
| User.hasMany(SocialAccount, { | ||
| foreignKey: 'userId', | ||
| as: 'socialAccounts', | ||
| }); | ||
|
|
||
| SocialAccount.belongsTo(User, { | ||
| foreignKey: 'userId', | ||
| as: 'user', | ||
| }); | ||
|
|
||
| module.exports = { SocialAccount }; |
There was a problem hiding this comment.
The task requires logout that is only possible for authenticated users and redirects to login after logging out. I couldn't find any server-side logout route in the backend files. Consider adding a DELETE/POST /logout route protected by authMiddleware that destroys server session if used and returns 204; the frontend currently clears local token client-side but an explicit server endpoint is needed if using server sessions.
| 'use strict'; | ||
|
|
||
| const { DataTypes } = require('sequelize'); | ||
| const { sequelize } = require('../db'); | ||
| const { User } = require('./user.model'); | ||
|
|
||
| const SocialAccount = sequelize.define( | ||
| 'SocialAccount', | ||
| { | ||
| id: { | ||
| type: DataTypes.INTEGER, | ||
| autoIncrement: true, | ||
| primaryKey: true, | ||
| }, | ||
| provider: { | ||
| type: DataTypes.STRING, | ||
| allowNull: false, | ||
| }, | ||
| providerUserId: { | ||
| type: DataTypes.STRING, | ||
| allowNull: false, | ||
| field: 'providerId', | ||
| }, | ||
| providerEmail: { | ||
| type: DataTypes.STRING, | ||
| allowNull: true, | ||
| field: 'providerEmail', | ||
| }, | ||
| userId: { | ||
| type: DataTypes.INTEGER, | ||
| allowNull: false, | ||
| field: 'user_id', | ||
| }, | ||
| }, | ||
| // We use these indexes to ensure uniqueness and improve query performance | ||
| { | ||
| tableName: 'social_accounts', |
There was a problem hiding this comment.
guestOnlyMiddleware and authMiddleware rely solely on Authorization: Bearer header. The frontend stores access token in localStorage and sets Authorization header in requests — this is consistent. However, some GitHub flows depend on express-session cookies (req.session.githubAuth). Ensure CORS credentials and cookie settings (in app.js session cookie and cors) allow session cookie on OAuth redirects (cookie.secure tied to NODE_ENV and sameSite: 'lax' may block cross-site POSTs in some browsers). This is operational guidance to ensure GitHub link/auth flows work.
|
|
||
| function guestOnlyMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
|
|
||
| if (!header) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { |
There was a problem hiding this comment.
The middleware correctly returns ApiError for ApiError instances and logs other errors. However, it exposes full error via process.stderr.write(${error}\n), which is fine for logs but ensure sensitive info isn't logged in production. (Informational)
| function guestOnlyMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
|
|
||
| if (!header) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const payload = jwtService.verify(token); | ||
|
|
||
| if (!payload) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| next(ApiError.badRequest('This action is only available when signed out')); |
There was a problem hiding this comment.
guestOnlyMiddleware inspects Authorization header to determine signed-in state. This approach works but relies solely on Authorization header; if the client uses cookie-based sessions, guestOnlyMiddleware won't detect them. Given the app uses JWT bearer tokens in API calls, this is acceptable. Consider documenting that guestOnly applies to Bearer-authenticated requests only.
| function guestOnlyMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
|
|
||
| if (!header) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const payload = jwtService.verify(token); | ||
|
|
||
| if (!payload) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| next(ApiError.badRequest('This action is only available when signed out')); |
There was a problem hiding this comment.
In guestOnlyMiddleware, the middleware calls next() in multiple branches and finally next(ApiError.badRequest(...)). Behavior is correct, but you may simplify flow for clarity. No functional change required.
| const { | ||
| validateName, |
There was a problem hiding this comment.
Apply guestOnlyMiddleware to POST /register so only non-authenticated users can register. Add it similarly to POST /login. Currently both routes are unprotected which violates requirement: "Register ... (only non authenticated)" and "Login ... (only non authenticated)".
| 'use strict'; | ||
|
|
||
| const { createHash } = require('crypto'); | ||
| const bcrypt = require('bcryptjs'); | ||
| const { v4: uuidv4 } = require('uuid'); | ||
| const jwtService = require('./jwt.service'); | ||
|
|
||
| const { User } = require('../models/user.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const { | ||
| validateName, | ||
| validateEmail, | ||
| validatePassword, | ||
| } = require('../utils/validate'); | ||
| const userService = require('./user.service'); | ||
| const mailService = require('./mail.service'); | ||
|
|
||
| // Hash the stored bcrypt hash so reset tokens expire after a password change | ||
| // without exposing the password hash itself inside the signed token payload. | ||
| function getPasswordResetFingerprint(passwordHash) { | ||
| return createHash('sha256') | ||
| .update(passwordHash || 'no-password') | ||
| .digest('hex'); | ||
| } | ||
|
|
||
| async function register({ name, email, password }) { | ||
| const errors = {}; | ||
|
|
||
| const nameError = validateName(name); | ||
| const emailError = validateEmail(email); | ||
| const passwordError = validatePassword(password); | ||
|
|
||
| if (nameError) { | ||
| errors.name = nameError; | ||
| } | ||
|
|
||
| if (emailError) { | ||
| errors.email = emailError; | ||
| } | ||
|
|
||
| if (passwordError) { | ||
| errors.password = passwordError; | ||
| } | ||
|
|
||
| if (Object.keys(errors).length > 0) { |
There was a problem hiding this comment.
Consider adding a server-side logout route protected by authMiddleware (e.g., POST/DELETE /logout) that destroys server session and/or informs client. The task requires "Logout (only authenticated)" and redirect to login after logging out. Currently there's no backend logout endpoint; frontend clears localStorage but server session destroy may be needed if sessions are used for OAuth flows.
| const errors = {}; | ||
|
|
||
| const nameError = validateName(name); | ||
| const emailError = validateEmail(email); | ||
| const passwordError = validatePassword(password); | ||
|
|
||
| if (nameError) { | ||
| errors.name = nameError; | ||
| } | ||
|
|
||
| if (emailError) { |
There was a problem hiding this comment.
Routes for password reset and activation are correctly protected with guestOnlyMiddleware where appropriate; ensure frontend flows use these (they do). No further route changes required beyond adding guestOnlyMiddleware to register/login.
|
|
||
| authRouter.post('/register', authController.register); | ||
| authRouter.post('/login', authController.login); |
There was a problem hiding this comment.
In auth.routes.js, POST /register and POST /login are not protected by guestOnlyMiddleware. The task requires registration and login to be available only for non‑authenticated users — add guestOnlyMiddleware to these routes (e.g., authRouter.post('/register', guestOnlyMiddleware, authController.register)).
| 'use strict'; | ||
|
|
||
| const express = require('express'); | ||
| const authController = require('../controllers/auth.controller'); | ||
| const { authMiddleware } = require('../middlewares/authMiddleware'); | ||
| const { guestOnlyMiddleware } = require('../middlewares/guestOnlyMiddleware'); | ||
|
|
||
| const authRouter = express.Router(); | ||
|
|
||
| authRouter.post('/register', authController.register); | ||
| authRouter.post('/login', authController.login); | ||
|
|
||
| authRouter.post( | ||
| '/auth/github/start', | ||
| guestOnlyMiddleware, | ||
| authController.startGithubAuthentication, | ||
| ); | ||
|
|
||
| authRouter.post( | ||
| '/auth/github/link/start', | ||
| authMiddleware, | ||
| authController.startGithubLink, | ||
| ); | ||
| authRouter.get('/auth/github', authController.githubAuthenticate); | ||
| authRouter.get('/auth/github/callback', authController.githubCallback); | ||
|
|
||
| authRouter.post( | ||
| '/password-reset', | ||
| guestOnlyMiddleware, | ||
| authController.requestPasswordReset, | ||
| ); | ||
|
|
||
| authRouter.post( | ||
| '/password-reset/confirm', | ||
| guestOnlyMiddleware, | ||
| authController.resetPassword, | ||
| ); | ||
| authRouter.get('/activate/:hash', authController.activate); | ||
|
|
||
| authRouter.get( | ||
| '/email-change/confirm/:token', | ||
| authController.confirmEmailChange, | ||
| ); | ||
|
|
||
| module.exports = { authRouter }; |
There was a problem hiding this comment.
There is no server-side logout route. The task requires logout to be available only for authenticated users and to redirect to login after logging out. Add an authenticated endpoint (e.g., POST/DELETE /logout) protected by authMiddleware that destroys server session (if any) and responds so frontend can redirect to /login.
| 'use strict'; | ||
|
|
||
| const express = require('express'); | ||
| const authController = require('../controllers/auth.controller'); | ||
| const { authMiddleware } = require('../middlewares/authMiddleware'); | ||
| const { guestOnlyMiddleware } = require('../middlewares/guestOnlyMiddleware'); | ||
|
|
||
| const authRouter = express.Router(); | ||
|
|
||
| authRouter.post('/register', authController.register); | ||
| authRouter.post('/login', authController.login); | ||
|
|
||
| authRouter.post( | ||
| '/auth/github/start', | ||
| guestOnlyMiddleware, | ||
| authController.startGithubAuthentication, | ||
| ); | ||
|
|
||
| authRouter.post( | ||
| '/auth/github/link/start', | ||
| authMiddleware, | ||
| authController.startGithubLink, | ||
| ); | ||
| authRouter.get('/auth/github', authController.githubAuthenticate); | ||
| authRouter.get('/auth/github/callback', authController.githubCallback); | ||
|
|
||
| authRouter.post( | ||
| '/password-reset', | ||
| guestOnlyMiddleware, | ||
| authController.requestPasswordReset, | ||
| ); | ||
|
|
||
| authRouter.post( | ||
| '/password-reset/confirm', |
There was a problem hiding this comment.
guestOnlyMiddleware determines guest status by checking Authorization header for a Bearer token. This matches frontend API usage which sets Authorization when access token exists. Ensure all guest-only pages/routes (frontend) also use the client-side guest check so UX matches server constraints. (Informational)
|
|
||
| authRouter.post( | ||
| '/auth/github/start', | ||
| guestOnlyMiddleware, | ||
| authController.startGithubAuthentication, | ||
| ); | ||
|
|
||
| authRouter.post( | ||
| '/auth/github/link/start', | ||
| authMiddleware, | ||
| authController.startGithubLink, | ||
| ); | ||
| authRouter.get('/auth/github', authController.githubAuthenticate); | ||
| authRouter.get('/auth/github/callback', authController.githubCallback); |
There was a problem hiding this comment.
GitHub OAuth start endpoints save flow state to req.session.githubAuth. Ensure express-session cookie settings and CORS credentials (configured in app.js) allow the session cookie to be sent during OAuth provider redirects; otherwise session state may be lost on callback. Confirm cookie.secure and sameSite settings are appropriate for your deployment. (Operational)
| userRouter.patch('/me/password', authMiddleware, userController.changePassword); | ||
| userRouter.patch('/me/name', authMiddleware, userController.changeName); |
There was a problem hiding this comment.
In auth.routes.js POST /register and POST /login are not protected by guestOnlyMiddleware. The task requires registration and login to be available only for non‑authenticated users. Add guestOnlyMiddleware to these routes, e.g.: authRouter.post('/register', guestOnlyMiddleware, authController.register) and similarly for /login.
| 'use strict'; | ||
|
|
||
| const express = require('express'); | ||
| const userController = require('../controllers/user.controller'); | ||
| const { authMiddleware } = require('../middlewares/authMiddleware'); | ||
|
|
||
| const userRouter = express.Router(); | ||
|
|
||
| userRouter.get('/me', authMiddleware, userController.me); | ||
| userRouter.patch('/me/password', authMiddleware, userController.changePassword); | ||
| userRouter.patch('/me/name', authMiddleware, userController.changeName); |
There was a problem hiding this comment.
There is no server-side logout route. The task requires logout only for authenticated users and redirect to login after logging out. Implement a protected endpoint (e.g., POST or DELETE /logout) that uses authMiddleware and destroys server session (req.session.destroy) and/or informs client to clear token. Frontend should call it and then redirect to /login.
|
|
||
| function getPrimaryEmail(profile) { |
There was a problem hiding this comment.
In auth.routes.js POST /register and /login are not protected by guestOnlyMiddleware. The task requires registration and login to be available only to non‑authenticated users. Add guestOnlyMiddleware to these routes (e.g., authRouter.post('/register', guestOnlyMiddleware, authController.register)).
| 'use strict'; | ||
|
|
||
| const { sequelize } = require('../db'); | ||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const jwtService = require('./jwt.service'); | ||
| const userService = require('./user.service'); | ||
| const { GITHUB_PROVIDER } = require('./socialAccount.service'); | ||
|
|
||
| function getPrimaryEmail(profile) { | ||
| const email = profile.emails.find(Boolean) || ''; | ||
|
|
||
| return String(email).trim(); | ||
| } | ||
|
|
||
| function getDisplayName(profile, email) { | ||
| const fallbackName = email.split('@')[0] || 'GitHub user'; | ||
|
|
||
| return String(profile.displayName || profile.username || fallbackName).trim(); | ||
| } | ||
|
|
||
| async function finalizeGitHubAuthentication(profile, linkUserId = null) { | ||
| const providerUserId = String(profile.providerUserId || '').trim(); | ||
|
|
||
| if (!providerUserId) { | ||
| throw ApiError.badRequest('GitHub did not return a valid user id.'); | ||
| } | ||
|
|
||
| const email = getPrimaryEmail(profile); | ||
|
|
||
| if (!email) { | ||
| throw ApiError.badRequest('Error'); | ||
| } | ||
|
|
||
| const name = getDisplayName(profile, email); | ||
|
|
||
| return sequelize.transaction(async (transaction) => { | ||
| const existingSocialAccount = await SocialAccount.findOne({ | ||
| where: { | ||
| provider: GITHUB_PROVIDER, | ||
| providerUserId, | ||
| }, | ||
| transaction, | ||
| }); |
There was a problem hiding this comment.
There is no server-side logout route. The task requires logout be available only for authenticated users and redirect to login after logging out. Add a logout endpoint (protected by authMiddleware) that destroys session (if used) and/or advises frontend to clear token, then returns 204 or a JSON response. Frontend should call it and redirect to /login.
| 'use strict'; | ||
|
|
||
| const { sequelize } = require('../db'); | ||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const jwtService = require('./jwt.service'); | ||
| const userService = require('./user.service'); | ||
| const { GITHUB_PROVIDER } = require('./socialAccount.service'); | ||
|
|
||
| function getPrimaryEmail(profile) { | ||
| const email = profile.emails.find(Boolean) || ''; | ||
|
|
||
| return String(email).trim(); | ||
| } | ||
|
|
||
| function getDisplayName(profile, email) { | ||
| const fallbackName = email.split('@')[0] || 'GitHub user'; | ||
|
|
||
| return String(profile.displayName || profile.username || fallbackName).trim(); | ||
| } |
There was a problem hiding this comment.
guestOnlyMiddleware and authMiddleware inspect only the Authorization header (Bearer token). This matches the frontend which sets the header from localStorage, but the GitHub OAuth flows rely on express-session cookies. Ensure CORS and session cookie settings allow the session cookie across the OAuth redirect (sameSite, secure and credentials settings). This is operational guidance to avoid losing OAuth flow state.
| }; | ||
| } | ||
|
|
||
| let user = await userService.findByEmailCaseInsensitive(email, { | ||
| transaction, | ||
| }); | ||
|
|
||
| if (!user) { | ||
| user = await User.create( | ||
| { | ||
| name, | ||
| email, |
There was a problem hiding this comment.
saveSession in auth.controller.js calls req.session.save without checking if req.session exists. Given express-session is configured this should be fine, but add a guard if (!req.session) return Promise.resolve(); for extra robustness.
| if (!providerUserId) { | ||
| throw ApiError.badRequest('GitHub did not return a valid user id.'); | ||
| } | ||
|
|
||
| const email = getPrimaryEmail(profile); | ||
|
|
||
| if (!email) { | ||
| throw ApiError.badRequest('Error'); | ||
| } | ||
|
|
||
| const name = getDisplayName(profile, email); | ||
|
|
||
| return sequelize.transaction(async (transaction) => { | ||
| const existingSocialAccount = await SocialAccount.findOne({ | ||
| where: { | ||
| provider: GITHUB_PROVIDER, | ||
| providerUserId, | ||
| }, | ||
| transaction, | ||
| }); | ||
|
|
||
| if (linkUserId) { | ||
| const linkedUser = await userService.findById(linkUserId, { | ||
| transaction, | ||
| }); | ||
|
|
||
| if (!linkedUser) { | ||
| throw ApiError.notFound('User not found'); | ||
| } | ||
|
|
||
| if (existingSocialAccount) { | ||
| if (existingSocialAccount.userId !== linkedUser.id) { | ||
| throw ApiError.badRequest( | ||
| 'This GitHub account is already linked to another user.', | ||
| ); | ||
| } | ||
|
|
||
| return { | ||
| accessToken: null, | ||
| message: 'GitHub is already connected to your account.', | ||
| redirectPath: '/account', | ||
| }; | ||
| } | ||
|
|
||
| await SocialAccount.create( | ||
| { | ||
| provider: GITHUB_PROVIDER, | ||
| providerUserId, | ||
| providerEmail: email, | ||
| userId: linkedUser.id, | ||
| }, | ||
| { transaction }, | ||
| ); | ||
|
|
||
| return { | ||
| accessToken: null, | ||
| message: 'GitHub account connected successfully.', | ||
| redirectPath: '/account', | ||
| }; | ||
| } | ||
|
|
||
| if (existingSocialAccount) { | ||
| const existingUser = await userService.findById( | ||
| existingSocialAccount.userId, | ||
| { | ||
| transaction, | ||
| }, |
There was a problem hiding this comment.
Activation and email-change redirects include the access token in the URL query string. This works with the frontend ActivationPage which reads and stores the token, but exposing tokens in query params can leak them via referer or browser history. You already issue short-lived tokens; consider documenting this behavior or switching to a fragment/hash or POST if stricter security is desired.
| } | ||
|
|
There was a problem hiding this comment.
POST /register and POST /login in auth.routes.js are not protected by guestOnlyMiddleware. The task requires registration and login to be available only to non‑authenticated users. Add guestOnlyMiddleware to these routes, e.g. authRouter.post('/register', guestOnlyMiddleware, authController.register) and similarly for /login.
| 'use strict'; | ||
|
|
||
| const Nodemailer = require('nodemailer'); | ||
|
|
||
| let transport; | ||
|
|
||
| function getTransport() { | ||
| if (transport) { | ||
| return transport; | ||
| } | ||
|
|
||
| const user = process.env.GMAIL_USER || process.env.MAIL_FROM_ADDRESS; | ||
| const pass = process.env.GMAIL_APP_PASSWORD; | ||
|
|
||
| if (!user || !pass) { | ||
| throw new Error( | ||
| 'GMAIL_USER (or MAIL_FROM_ADDRESS) and GMAIL_APP_PASSWORD' + | ||
| ' are required to send emails through Gmail SMTP', | ||
| ); | ||
| } | ||
|
|
||
| transport = Nodemailer.createTransport({ | ||
| host: 'smtp.gmail.com', | ||
| port: 465, | ||
| secure: true, | ||
| auth: { | ||
| user, | ||
| pass, | ||
| }, | ||
| }); | ||
|
|
||
| return transport; | ||
| } | ||
|
|
||
| function getSender() { | ||
| const address = process.env.MAIL_FROM_ADDRESS || process.env.GMAIL_USER; | ||
|
|
||
| if (!address) { | ||
| throw new Error( | ||
| 'MAIL_FROM_ADDRESS or GMAIL_USER is required for the sender address', | ||
| ); | ||
| } | ||
|
|
||
| return { | ||
| address, |
There was a problem hiding this comment.
There is no server-side logout route. The task requires logout (only authenticated) and redirect to login after logging out. Add a protected logout endpoint (e.g., POST or DELETE /logout) using authMiddleware that destroys the server session (if used) and returns success so the frontend can clear token and redirect to /login.
| subject: 'Reset your password', | ||
| text: `Follow this link to reset your password: ${link}`, | ||
| html: ` | ||
| <h2>Reset your password</h2> | ||
| <p>Follow the link below to choose a new password:</p> | ||
| <a href="${link}">${link}</a> | ||
| `, | ||
| }); | ||
| } | ||
|
|
||
| module.exports = { | ||
| sendActivationLink, |
There was a problem hiding this comment.
saveSession in auth.controller.js calls req.session.save without checking if req.session exists. Express-session is configured so this is unlikely, but guarding if (!req.session) return Promise.resolve(); would make it more robust.
| auth: { | ||
| user, | ||
| pass, | ||
| }, | ||
| }); | ||
|
|
||
| return transport; | ||
| } | ||
|
|
||
| function getSender() { | ||
| const address = process.env.MAIL_FROM_ADDRESS || process.env.GMAIL_USER; | ||
|
|
||
| if (!address) { | ||
| throw new Error( | ||
| 'MAIL_FROM_ADDRESS or GMAIL_USER is required for the sender address', | ||
| ); | ||
| } | ||
|
|
||
| return { | ||
| address, | ||
| name: process.env.MAIL_FROM_NAME || 'Auth App', | ||
| }; | ||
| } | ||
|
|
||
| function send({ to, subject, text, html }) { | ||
| return getTransport().sendMail({ | ||
| from: getSender(), | ||
| to, | ||
| subject, | ||
| text, | ||
| html, | ||
| }); | ||
| } | ||
|
|
||
| function sendActivationLink(email, hash) { | ||
| const serverUrl = process.env.SERVER_URL || 'http://localhost:4000'; | ||
| const link = `${serverUrl}/activate/${hash}`; | ||
|
|
||
| return send({ | ||
| to: email, | ||
| subject: 'Activate your account', | ||
| text: `Follow this link to activate your account: ${link}`, | ||
| html: ` | ||
| <h2>Activate your account</h2> | ||
| <p>Follow the link below to activate your account:</p> | ||
| <a href="${link}">${link}</a> | ||
| `, | ||
| }); | ||
| } | ||
|
|
||
| function sendEmailChangeConfirmation(email, token) { | ||
| const serverUrl = process.env.SERVER_URL || 'http://localhost:4000'; | ||
| const link = `${serverUrl}/email-change/confirm/${encodeURIComponent(token)}`; | ||
|
|
||
| return send({ | ||
| to: email, | ||
| subject: 'Confirm your new email address', | ||
| text: `Follow this link to confirm your new email address: ${link}`, | ||
| html: ` | ||
| <h2>Confirm your new email address</h2> | ||
| <p>Follow the link below to confirm your new email address:</p> | ||
| <a href="${link}">${link}</a> | ||
| `, | ||
| }); | ||
| } | ||
|
|
||
| function sendEmailChangeNotification(email, newEmail) { |
There was a problem hiding this comment.
Activation and email-change callbacks redirect to the frontend with an access token in the URL query. Frontend ActivationPage consumes this token. Note: exposing tokens in query strings can leak them via referer/history; you already issue short-lived tokens, but consider documenting this design or using a fragment/hash or POST if stricter protection is desired.
| where: { provider, providerUserId }, | ||
| ...options, | ||
| }); | ||
| } | ||
|
|
||
| function findByUserAndProvider(userId, provider, options = {}) { | ||
| return SocialAccount.findOne({ |
There was a problem hiding this comment.
In jwt.service.js, sign and verification functions use environment secrets (e.g., process.env.JWT_ACCESS_SECRET) without fallback to a default value. If these env vars are missing, jwt.sign will throw. Consider validating required secrets at startup or providing clearer errors. This is important because many flows depend on JWT operations (access, email-change, password-reset).
| provider: account.provider, | ||
| }; | ||
| } | ||
|
|
||
| function findByProviderAccount(provider, providerUserId, options = {}) { | ||
| return SocialAccount.findOne({ | ||
| where: { provider, providerUserId }, | ||
| ...options, | ||
| }); | ||
| } |
There was a problem hiding this comment.
In mail.service.js getTransport throws if MAIL_FROM_ADDRESS/GMAIL_USER or GMAIL_APP_PASSWORD are missing. During development/tests this will break registration/password reset flows that send emails. Consider adding a noop transport or a clear error message indicating emails are disabled in non-production. (Informational but can block local testing)
| const linkedAccountsCount = await SocialAccount.count({ | ||
| where: { userId }, | ||
| }); | ||
|
|
||
| if (!user.password && linkedAccountsCount <= 1) { | ||
| throw ApiError.badRequest( | ||
| 'Set a password before removing your only social sign-in method.', | ||
| ); | ||
| } | ||
|
|
||
| await socialAccount.destroy(); |
There was a problem hiding this comment.
In socialAccount.service.removeByUserAndProvider you prevent removing the last sign-in method when user has no password. This enforces safety for social-account-only accounts — it matches requirements to require setting a password before removing only social sign-in. Good.
| try { | ||
| return jwt.verify(token, secret); |
There was a problem hiding this comment.
POST /register is currently unprotected. The task requires registration be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.:
authRouter.post('/register', guestOnlyMiddleware, authController.register);
| try { | ||
| return jwt.verify(token, secret); |
There was a problem hiding this comment.
POST /login is currently unprotected. The task requires login be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.:
authRouter.post('/login', guestOnlyMiddleware, authController.login);
| 'use strict'; | ||
|
|
||
| const jwt = require('jsonwebtoken'); | ||
|
|
||
| function signToken(payload, secret, expiresIn) { | ||
| return jwt.sign(payload, secret, { expiresIn }); | ||
| } | ||
|
|
||
| function verifyToken(token, secret) { | ||
| try { | ||
| return jwt.verify(token, secret); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| function sign(payload) { | ||
| return signToken( | ||
| payload, | ||
| process.env.JWT_ACCESS_SECRET, | ||
| process.env.JWT_ACCESS_EXPIRES || '1d', | ||
| ); | ||
| } | ||
|
|
||
| function verify(token) { | ||
| return verifyToken(token, process.env.JWT_ACCESS_SECRET); | ||
| } | ||
|
|
||
| function signEmailChangeToken(payload) { | ||
| return signToken( | ||
| payload, | ||
| process.env.JWT_EMAIL_CHANGE_SECRET || process.env.JWT_ACCESS_SECRET, | ||
| process.env.JWT_EMAIL_CHANGE_EXPIRES || '1h', | ||
| ); | ||
| } | ||
|
|
||
| function verifyEmailChangeToken(token) { | ||
| return verifyToken( | ||
| token, | ||
| process.env.JWT_EMAIL_CHANGE_SECRET || process.env.JWT_ACCESS_SECRET, | ||
| ); | ||
| } | ||
|
|
||
| function signPasswordResetToken(payload) { | ||
| return signToken( |
There was a problem hiding this comment.
There is no server-side logout route. The task requires logout (only authenticated) and redirect to login after logging out. Add a protected logout endpoint (use authMiddleware) that destroys the session (if any) and returns success so the frontend can clear token and navigate to /login.
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| function sign(payload) { | ||
| return signToken( | ||
| payload, | ||
| process.env.JWT_ACCESS_SECRET, | ||
| process.env.JWT_ACCESS_EXPIRES || '1d', | ||
| ); | ||
| } | ||
|
|
||
| function verify(token) { |
There was a problem hiding this comment.
GitHub OAuth flows rely on express-session cookie persistence across provider redirects (req.session.githubAuth). Ensure CORS and session cookie settings allow the cookie on the OAuth callback (app.js sets credentials:true and session cookie sameSite:'lax' — verify this works in your deployment). If cookies are lost, OAuth flow may fallback to authenticate intent.
| const { | ||
| validatePassword, |
There was a problem hiding this comment.
auth.routes.js: POST /register and POST /login are not using guestOnlyMiddleware. The task requires registration and login to be available only for non‑authenticated users — add guestOnlyMiddleware to these routes (e.g., authRouter.post('/register', guestOnlyMiddleware, authController.register)).
| /* eslint-disable indent */ | ||
| 'use strict'; | ||
|
|
||
| const bcrypt = require('bcryptjs'); | ||
| const { col, fn, where } = require('sequelize'); | ||
|
|
||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const { | ||
| validatePassword, | ||
| validateName, | ||
| validateEmail, | ||
| } = require('../utils/validate'); | ||
| const jwtService = require('./jwt.service'); | ||
| const mailService = require('./mail.service'); | ||
|
|
||
| function normalize(user) { | ||
| return { | ||
| id: user.id, | ||
| name: user.name, | ||
| email: user.email, | ||
| hasPassword: Boolean(user.password), | ||
| socialAccounts: Array.isArray(user.socialAccounts) | ||
| ? user.socialAccounts.map((socialAccount) => ({ | ||
| provider: socialAccount.provider, | ||
| })) | ||
| : [], | ||
| }; | ||
| } | ||
|
|
||
| function findByEmail(email, options = {}) { | ||
| return User.findOne({ where: { email }, ...options }); | ||
| } | ||
|
|
||
| function findByEmailCaseInsensitive(email, options = {}) { | ||
| return User.findOne({ | ||
| where: where(fn('lower', col('email')), String(email).trim().toLowerCase()), | ||
| ...options, | ||
| }); | ||
| } | ||
|
|
||
| function findByActivationToken(activationToken, options = {}) { | ||
| return User.findOne({ where: { activationToken }, ...options }); | ||
| } |
There was a problem hiding this comment.
No logout route exists in auth.routes.js or controllers. The task requires logout only for authenticated users and redirect to login after logging out. Add a protected logout endpoint (use authMiddleware) that destroys server session (if used) and returns success so frontend can clear token and navigate to /login.
| const nameError = validateName(newName); | ||
|
|
||
| if (nameError) { | ||
| throw ApiError.badRequest('Validation failed', { newName: nameError }); | ||
| } | ||
|
|
||
| const user = await findById(userId); | ||
|
|
||
| if (!user) { | ||
| throw ApiError.notFound('User not found'); | ||
| } | ||
|
|
There was a problem hiding this comment.
saveSession in auth.controller.js calls req.session.save without guarding for missing req.session. Express-session is configured, but for robustness consider adding if (!req.session) return Promise.resolve(); at start of saveSession to avoid potential runtime errors.
| throw ApiError.badRequest( | ||
| 'Set a password before changing your email address.', | ||
| { | ||
| password: 'Set a password before changing your email address.', | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| const errors = {}; | ||
| const normalizedEmail = String(newEmail).trim(); | ||
|
|
||
| if (!password) { | ||
| errors.password = 'Password is required'; | ||
| } | ||
|
|
||
| const emailError = validateEmail(normalizedEmail); | ||
|
|
There was a problem hiding this comment.
GitHub OAuth flows store intent in req.session.githubAuth. Ensure express-session cookie settings and CORS (credentials: true) permit the session cookie to be sent across OAuth redirects; otherwise session state may be lost on callback and flow will fallback. Verify sameSite/secure in production.
|
|
||
| function normalize(user) { | ||
| return { | ||
| id: user.id, | ||
| name: user.name, | ||
| email: user.email, | ||
| hasPassword: Boolean(user.password), |
There was a problem hiding this comment.
jwt.service.js: signing functions use environment variables directly (e.g., process.env.JWT_ACCESS_SECRET). If these are undefined, jwt.sign will throw. Ensure required JWT secrets are set in env for production. Consider validating presence at startup or providing clearer errors.
| validatePassword, | ||
| validateName, | ||
| validateEmail, | ||
| } = require('../utils/validate'); | ||
| const jwtService = require('./jwt.service'); | ||
| const mailService = require('./mail.service'); | ||
|
|
||
| function normalize(user) { | ||
| return { |
There was a problem hiding this comment.
mail.service.js: getTransport throws if GMAIL_USER / GMAIL_APP_PASSWORD missing. For development/testing consider a noop/mock transport to avoid runtime failures when sending emails is not configured.
|
|
||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const { | ||
| validatePassword, | ||
| validateName, | ||
| validateEmail, | ||
| } = require('../utils/validate'); | ||
| const jwtService = require('./jwt.service'); | ||
| const mailService = require('./mail.service'); | ||
|
|
||
| function normalize(user) { | ||
| return { | ||
| id: user.id, | ||
| name: user.name, | ||
| email: user.email, | ||
| hasPassword: Boolean(user.password), | ||
| socialAccounts: Array.isArray(user.socialAccounts) | ||
| ? user.socialAccounts.map((socialAccount) => ({ | ||
| provider: socialAccount.provider, | ||
| })) | ||
| : [], | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
guestOnlyMiddleware determines guest status solely from Authorization: Bearer header. This is consistent with frontend using bearer tokens. If you later support cookie/session auth for API calls, update this middleware to detect that too. For current app behavior no change required.
| function findByEmailCaseInsensitive(email, options = {}) { | ||
| return User.findOne({ | ||
| where: where(fn('lower', col('email')), String(email).trim().toLowerCase()), | ||
| ...options, | ||
| }); | ||
| } | ||
|
|
||
| function findByActivationToken(activationToken, options = {}) { | ||
| return User.findOne({ where: { activationToken }, ...options }); | ||
| } | ||
|
|
There was a problem hiding this comment.
socialAccount.model.js: indexes reference physical column names via field mappings (e.g., providerId, user_id). Ensure DB migrations/schema use the same column names so uniqueness indexes are applied correctly. This is informational — model is consistent as written.
| static badRequest(message, errors) { | ||
| return new ApiError({ status: 400, message, errors }); |
There was a problem hiding this comment.
POST /register is currently unprotected. The task requires registration be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.:
authRouter.post('/register', guestOnlyMiddleware, authController.register);
| static badRequest(message, errors) { | ||
| return new ApiError({ status: 400, message, errors }); |
There was a problem hiding this comment.
POST /login is currently unprotected. The task requires login be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.:
authRouter.post('/login', guestOnlyMiddleware, authController.login);
| node_modules | ||
| dist |
There was a problem hiding this comment.
auth.routes.js: POST /register is not protected by guestOnlyMiddleware. The task requires registration to be available only for non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/register', guestOnlyMiddleware, authController.register);
| node_modules | ||
| dist |
There was a problem hiding this comment.
auth.routes.js: POST /login is not protected by guestOnlyMiddleware. The task requires login to be available only for non‑authenticated users. Add guestOnlyMiddleware to this route as well: authRouter.post('/login', guestOnlyMiddleware, authController.login);
| !.vscode/extensions.json | ||
| .idea | ||
| .DS_Store | ||
| *.suo | ||
| *.ntvs* | ||
| *.njsproj | ||
| *.sln |
There was a problem hiding this comment.
jwt.service.js: JWT signing functions rely on environment secrets (e.g., JWT_ACCESS_SECRET) and will throw if missing. To avoid runtime failures, validate required JWT secrets at startup or provide clearer errors. This impacts activation/login/password-reset flows which depend on JWTs.
| dist | ||
| dist-ssr | ||
| *.local | ||
|
|
||
| # Editor directories and files | ||
| .vscode/* | ||
| !.vscode/extensions.json | ||
| .idea | ||
| .DS_Store | ||
| *.suo |
There was a problem hiding this comment.
mail.service.js: getTransport throws when MAIL_FROM_ADDRESS/GMAIL_USER or GMAIL_APP_PASSWORD are missing. During development or tests this will break flows that send emails (registration, password reset). Consider providing a development/noop transport (or Ethereal) to allow testing without real SMTP credentials.
| return null; | ||
| } |
There was a problem hiding this comment.
POST /register is currently unprotected. The task requires registration to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/register', guestOnlyMiddleware, authController.register);
| return null; | ||
| } |
There was a problem hiding this comment.
POST /login is currently unprotected. The task requires login to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/login', guestOnlyMiddleware, authController.login);
|
|
||
| function validateEmail(email) { | ||
| if (!email || !EMAIL_REGEX.test(email)) { | ||
| return 'Invalid email address'; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| function validatePassword(password) { | ||
| if (!password || password.length < 12) { | ||
| return 'Password must be at least 12 characters'; | ||
| } | ||
|
|
There was a problem hiding this comment.
The GitHub OAuth flow stores intent in req.session.githubAuth. Ensure express-session cookie and CORS settings allow the session cookie to be sent during provider redirect and callback (app.js sets credentials: true and session cookie sameSite:'lax'). If the session cookie is lost on callback, the OAuth flow may fallback to authenticate intent — verify this in integration tests.
|
|
||
| return null; | ||
| } | ||
|
|
||
| function validatePassword(password) { | ||
| if (!password || password.length < 12) { | ||
| return 'Password must be at least 12 characters'; |
There was a problem hiding this comment.
jwt.service.js relies on environment secrets (e.g., JWT_ACCESS_SECRET). If these are missing jwt.sign will throw. Consider validating required JWT env vars at startup and providing clear errors to avoid runtime failures for token operations used across many flows.
| } | ||
|
|
||
| function validateEmail(email) { | ||
| if (!email || !EMAIL_REGEX.test(email)) { | ||
| return 'Invalid email address'; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
There was a problem hiding this comment.
mail.service.js throws when MAIL or GMAIL env vars are missing, which will break registration/password-reset flows during development. Consider adding a development/noop transport or using a test SMTP provider to avoid runtime failures when email credentials are not configured.
| ## React Compiler | ||
|
|
There was a problem hiding this comment.
Add guestOnlyMiddleware to POST /register so only non-authenticated users can register. For example: authRouter.post('/register', guestOnlyMiddleware, authController.register); This is required by the task.
| ## React Compiler | ||
|
|
There was a problem hiding this comment.
Add guestOnlyMiddleware to POST /login so only non-authenticated users can log in. For example: authRouter.post('/login', guestOnlyMiddleware, authController.login); This is required by the task.
| # React + TypeScript + Vite | ||
|
|
||
| This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. | ||
|
|
||
| Currently, two official plugins are available: | ||
|
|
||
| - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) | ||
| - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) | ||
|
|
||
| ## React Compiler | ||
|
|
||
| The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). | ||
|
|
||
| ## Expanding the ESLint configuration | ||
|
|
||
| If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [ | ||
| // Other configs... | ||
|
|
||
| // Remove tseslint.configs.recommended and replace with this | ||
| tseslint.configs.recommendedTypeChecked, | ||
| // Alternatively, use this for stricter rules | ||
| tseslint.configs.strictTypeChecked, | ||
| // Optionally, add this for stylistic rules | ||
| tseslint.configs.stylisticTypeChecked, | ||
|
|
||
| // Other configs... | ||
| ], | ||
| languageOptions: { | ||
| parserOptions: { | ||
| project: ['./tsconfig.node.json', './tsconfig.app.json'], | ||
| tsconfigRootDir: import.meta.dirname, | ||
| }, | ||
| // other options... | ||
| }, | ||
| }, | ||
| ]) | ||
| ``` | ||
|
|
There was a problem hiding this comment.
There is no server-side logout route. The task requires logout to be available only for authenticated users and to redirect to login after logging out. Add a protected logout endpoint (use authMiddleware) that destroys server session (if used) and returns success so frontend can clear token and redirect to /login.
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [ |
There was a problem hiding this comment.
jwt.service.js relies on environment secrets such as JWT_ACCESS_SECRET. If these env vars are missing jwt.sign will throw. Validate required JWT secrets at startup or provide clear errors to avoid runtime failures in token operations (activation, reset, email-change).
|
|
||
| The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). | ||
|
|
||
| ## Expanding the ESLint configuration | ||
|
|
||
| If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), |
There was a problem hiding this comment.
mail.service.js throws when mail credentials are missing (GMAIL_USER / GMAIL_APP_PASSWORD). During development/tests this will break flows that send emails. Consider adding a development/noop transport or using an Ethereal account for local testing to avoid runtime exceptions.
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Review Summary
I am requesting changes because a few checklist-level requirements are not yet enforced on the backend, even though the overall implementation is otherwise strong and feature-complete.
Critical issues that must be fixed (blockers):
- Guest-only protection on auth routes: In
src/backend/src/routes/auth.routes.js, the routesPOST /registerandPOST /login(around lines 9–11) are currently unguarded. The task explicitly requires that registration and login be available only to non‑authenticated users, so you need to insertguestOnlyMiddlewarebefore the controllers for both of these routes (e.g.authRouter.post('/register', guestOnlyMiddleware, authController.register)). Similarly,GET /activate/:hash(around line 38) must be available only to guests; wrap that route withguestOnlyMiddlewareas well. - Missing server-side logout endpoint: The requirements state that logout must be available only for authenticated users and redirect to the login page after logging out, but there is no backend logout route. Add an authenticated endpoint such as
POST /logoutinauth.routes.js, protect it withauthMiddleware, and in its controller destroy the server session (using your existing session helper orreq.session.destroy) so the frontend can call it, then clear the token and navigate to/login.
Important to verify (may also be a blocker if incomplete):
3. Email-change initiation behavior: user.controller.changeEmail delegates to userService.requestEmailChange; per the checklist, this flow must (a) verify the current password, (b) create an email-change token with jwtService.signEmailChangeToken, (c) send a confirmation link to the new email using mailService.sendEmailChangeConfirmation, and (d) notify the old email using mailService.sendEmailChangeNotification. Please ensure this service method exists and performs all these steps; if it’s missing or partial, implement the missing pieces.
Everything else—registration, activation, login, password reset, profile updates, and GitHub OAuth—is very well structured and aligns closely with the requirements. Once you add the guest-only guards, the logout route, and confirm the email-change initiation logic, your solution will fully meet the task’s standards.
✨ 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
| function authMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { | ||
| return next(ApiError.unauthorized()); | ||
| } | ||
|
|
||
| const payload = jwtService.verify(token); | ||
|
|
||
| if (!payload) { | ||
| return next(ApiError.unauthorized()); | ||
| } | ||
|
|
||
| req.user = payload; | ||
| next(); |
There was a problem hiding this comment.
authMiddleware expects an Authorization header with Bearer token. However many frontend requests use access token stored in localStorage and set Authorization header in api.request; this is consistent. Ensure any requests relying on cookie-based session authentication use appropriate middleware — currently API uses bearer JWT, not session auth.
|
|
||
| function guestOnlyMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
|
|
||
| if (!header) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { |
There was a problem hiding this comment.
The middleware correctly returns ApiError for ApiError instances and logs other errors. However, it exposes full error via process.stderr.write(${error}\n), which is fine for logs but ensure sensitive info isn't logged in production. (Informational)
| function guestOnlyMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
|
|
||
| if (!header) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const payload = jwtService.verify(token); | ||
|
|
||
| if (!payload) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| next(ApiError.badRequest('This action is only available when signed out')); |
There was a problem hiding this comment.
guestOnlyMiddleware inspects Authorization header to determine signed-in state. This approach works but relies solely on Authorization header; if the client uses cookie-based sessions, guestOnlyMiddleware won't detect them. Given the app uses JWT bearer tokens in API calls, this is acceptable. Consider documenting that guestOnly applies to Bearer-authenticated requests only.
| function guestOnlyMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
|
|
||
| if (!header) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const payload = jwtService.verify(token); | ||
|
|
||
| if (!payload) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| next(ApiError.badRequest('This action is only available when signed out')); |
There was a problem hiding this comment.
In guestOnlyMiddleware, the middleware calls next() in multiple branches and finally next(ApiError.badRequest(...)). Behavior is correct, but you may simplify flow for clarity. No functional change required.
|
|
||
| function getPrimaryEmail(profile) { |
There was a problem hiding this comment.
In auth.routes.js POST /register and /login are not protected by guestOnlyMiddleware. The task requires registration and login to be available only to non‑authenticated users. Add guestOnlyMiddleware to these routes (e.g., authRouter.post('/register', guestOnlyMiddleware, authController.register)).
| 'use strict'; | ||
|
|
||
| const { sequelize } = require('../db'); | ||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const jwtService = require('./jwt.service'); | ||
| const userService = require('./user.service'); | ||
| const { GITHUB_PROVIDER } = require('./socialAccount.service'); | ||
|
|
||
| function getPrimaryEmail(profile) { | ||
| const email = profile.emails.find(Boolean) || ''; | ||
|
|
||
| return String(email).trim(); | ||
| } | ||
|
|
||
| function getDisplayName(profile, email) { | ||
| const fallbackName = email.split('@')[0] || 'GitHub user'; | ||
|
|
||
| return String(profile.displayName || profile.username || fallbackName).trim(); | ||
| } | ||
|
|
||
| async function finalizeGitHubAuthentication(profile, linkUserId = null) { | ||
| const providerUserId = String(profile.providerUserId || '').trim(); | ||
|
|
||
| if (!providerUserId) { | ||
| throw ApiError.badRequest('GitHub did not return a valid user id.'); | ||
| } | ||
|
|
||
| const email = getPrimaryEmail(profile); | ||
|
|
||
| if (!email) { | ||
| throw ApiError.badRequest('Error'); | ||
| } | ||
|
|
||
| const name = getDisplayName(profile, email); | ||
|
|
||
| return sequelize.transaction(async (transaction) => { | ||
| const existingSocialAccount = await SocialAccount.findOne({ | ||
| where: { | ||
| provider: GITHUB_PROVIDER, | ||
| providerUserId, | ||
| }, | ||
| transaction, | ||
| }); |
There was a problem hiding this comment.
There is no server-side logout route. The task requires logout be available only for authenticated users and redirect to login after logging out. Add a logout endpoint (protected by authMiddleware) that destroys session (if used) and/or advises frontend to clear token, then returns 204 or a JSON response. Frontend should call it and redirect to /login.
| 'use strict'; | ||
|
|
||
| const { sequelize } = require('../db'); | ||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const jwtService = require('./jwt.service'); | ||
| const userService = require('./user.service'); | ||
| const { GITHUB_PROVIDER } = require('./socialAccount.service'); | ||
|
|
||
| function getPrimaryEmail(profile) { | ||
| const email = profile.emails.find(Boolean) || ''; | ||
|
|
||
| return String(email).trim(); | ||
| } | ||
|
|
||
| function getDisplayName(profile, email) { | ||
| const fallbackName = email.split('@')[0] || 'GitHub user'; | ||
|
|
||
| return String(profile.displayName || profile.username || fallbackName).trim(); | ||
| } |
There was a problem hiding this comment.
guestOnlyMiddleware and authMiddleware inspect only the Authorization header (Bearer token). This matches the frontend which sets the header from localStorage, but the GitHub OAuth flows rely on express-session cookies. Ensure CORS and session cookie settings allow the session cookie across the OAuth redirect (sameSite, secure and credentials settings). This is operational guidance to avoid losing OAuth flow state.
| }; | ||
| } | ||
|
|
||
| let user = await userService.findByEmailCaseInsensitive(email, { | ||
| transaction, | ||
| }); | ||
|
|
||
| if (!user) { | ||
| user = await User.create( | ||
| { | ||
| name, | ||
| email, |
There was a problem hiding this comment.
saveSession in auth.controller.js calls req.session.save without checking if req.session exists. Given express-session is configured this should be fine, but add a guard if (!req.session) return Promise.resolve(); for extra robustness.
| if (!providerUserId) { | ||
| throw ApiError.badRequest('GitHub did not return a valid user id.'); | ||
| } | ||
|
|
||
| const email = getPrimaryEmail(profile); | ||
|
|
||
| if (!email) { | ||
| throw ApiError.badRequest('Error'); | ||
| } | ||
|
|
||
| const name = getDisplayName(profile, email); | ||
|
|
||
| return sequelize.transaction(async (transaction) => { | ||
| const existingSocialAccount = await SocialAccount.findOne({ | ||
| where: { | ||
| provider: GITHUB_PROVIDER, | ||
| providerUserId, | ||
| }, | ||
| transaction, | ||
| }); | ||
|
|
||
| if (linkUserId) { | ||
| const linkedUser = await userService.findById(linkUserId, { | ||
| transaction, | ||
| }); | ||
|
|
||
| if (!linkedUser) { | ||
| throw ApiError.notFound('User not found'); | ||
| } | ||
|
|
||
| if (existingSocialAccount) { | ||
| if (existingSocialAccount.userId !== linkedUser.id) { | ||
| throw ApiError.badRequest( | ||
| 'This GitHub account is already linked to another user.', | ||
| ); | ||
| } | ||
|
|
||
| return { | ||
| accessToken: null, | ||
| message: 'GitHub is already connected to your account.', | ||
| redirectPath: '/account', | ||
| }; | ||
| } | ||
|
|
||
| await SocialAccount.create( | ||
| { | ||
| provider: GITHUB_PROVIDER, | ||
| providerUserId, | ||
| providerEmail: email, | ||
| userId: linkedUser.id, | ||
| }, | ||
| { transaction }, | ||
| ); | ||
|
|
||
| return { | ||
| accessToken: null, | ||
| message: 'GitHub account connected successfully.', | ||
| redirectPath: '/account', | ||
| }; | ||
| } | ||
|
|
||
| if (existingSocialAccount) { | ||
| const existingUser = await userService.findById( | ||
| existingSocialAccount.userId, | ||
| { | ||
| transaction, | ||
| }, |
There was a problem hiding this comment.
Activation and email-change redirects include the access token in the URL query string. This works with the frontend ActivationPage which reads and stores the token, but exposing tokens in query params can leak them via referer or browser history. You already issue short-lived tokens; consider documenting this behavior or switching to a fragment/hash or POST if stricter security is desired.
| const { | ||
| validatePassword, |
There was a problem hiding this comment.
auth.routes.js: POST /register and POST /login are not using guestOnlyMiddleware. The task requires registration and login to be available only for non‑authenticated users — add guestOnlyMiddleware to these routes (e.g., authRouter.post('/register', guestOnlyMiddleware, authController.register)).
| /* eslint-disable indent */ | ||
| 'use strict'; | ||
|
|
||
| const bcrypt = require('bcryptjs'); | ||
| const { col, fn, where } = require('sequelize'); | ||
|
|
||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const { | ||
| validatePassword, | ||
| validateName, | ||
| validateEmail, | ||
| } = require('../utils/validate'); | ||
| const jwtService = require('./jwt.service'); | ||
| const mailService = require('./mail.service'); | ||
|
|
||
| function normalize(user) { | ||
| return { | ||
| id: user.id, | ||
| name: user.name, | ||
| email: user.email, | ||
| hasPassword: Boolean(user.password), | ||
| socialAccounts: Array.isArray(user.socialAccounts) | ||
| ? user.socialAccounts.map((socialAccount) => ({ | ||
| provider: socialAccount.provider, | ||
| })) | ||
| : [], | ||
| }; | ||
| } | ||
|
|
||
| function findByEmail(email, options = {}) { | ||
| return User.findOne({ where: { email }, ...options }); | ||
| } | ||
|
|
||
| function findByEmailCaseInsensitive(email, options = {}) { | ||
| return User.findOne({ | ||
| where: where(fn('lower', col('email')), String(email).trim().toLowerCase()), | ||
| ...options, | ||
| }); | ||
| } | ||
|
|
||
| function findByActivationToken(activationToken, options = {}) { | ||
| return User.findOne({ where: { activationToken }, ...options }); | ||
| } |
There was a problem hiding this comment.
No logout route exists in auth.routes.js or controllers. The task requires logout only for authenticated users and redirect to login after logging out. Add a protected logout endpoint (use authMiddleware) that destroys server session (if used) and returns success so frontend can clear token and navigate to /login.
| const nameError = validateName(newName); | ||
|
|
||
| if (nameError) { | ||
| throw ApiError.badRequest('Validation failed', { newName: nameError }); | ||
| } | ||
|
|
||
| const user = await findById(userId); | ||
|
|
||
| if (!user) { | ||
| throw ApiError.notFound('User not found'); | ||
| } | ||
|
|
There was a problem hiding this comment.
saveSession in auth.controller.js calls req.session.save without guarding for missing req.session. Express-session is configured, but for robustness consider adding if (!req.session) return Promise.resolve(); at start of saveSession to avoid potential runtime errors.
| throw ApiError.badRequest( | ||
| 'Set a password before changing your email address.', | ||
| { | ||
| password: 'Set a password before changing your email address.', | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| const errors = {}; | ||
| const normalizedEmail = String(newEmail).trim(); | ||
|
|
||
| if (!password) { | ||
| errors.password = 'Password is required'; | ||
| } | ||
|
|
||
| const emailError = validateEmail(normalizedEmail); | ||
|
|
There was a problem hiding this comment.
GitHub OAuth flows store intent in req.session.githubAuth. Ensure express-session cookie settings and CORS (credentials: true) permit the session cookie to be sent across OAuth redirects; otherwise session state may be lost on callback and flow will fallback. Verify sameSite/secure in production.
|
|
||
| function normalize(user) { | ||
| return { | ||
| id: user.id, | ||
| name: user.name, | ||
| email: user.email, | ||
| hasPassword: Boolean(user.password), |
There was a problem hiding this comment.
jwt.service.js: signing functions use environment variables directly (e.g., process.env.JWT_ACCESS_SECRET). If these are undefined, jwt.sign will throw. Ensure required JWT secrets are set in env for production. Consider validating presence at startup or providing clearer errors.
| validatePassword, | ||
| validateName, | ||
| validateEmail, | ||
| } = require('../utils/validate'); | ||
| const jwtService = require('./jwt.service'); | ||
| const mailService = require('./mail.service'); | ||
|
|
||
| function normalize(user) { | ||
| return { |
There was a problem hiding this comment.
mail.service.js: getTransport throws if GMAIL_USER / GMAIL_APP_PASSWORD missing. For development/testing consider a noop/mock transport to avoid runtime failures when sending emails is not configured.
|
|
||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const { | ||
| validatePassword, | ||
| validateName, | ||
| validateEmail, | ||
| } = require('../utils/validate'); | ||
| const jwtService = require('./jwt.service'); | ||
| const mailService = require('./mail.service'); | ||
|
|
||
| function normalize(user) { | ||
| return { | ||
| id: user.id, | ||
| name: user.name, | ||
| email: user.email, | ||
| hasPassword: Boolean(user.password), | ||
| socialAccounts: Array.isArray(user.socialAccounts) | ||
| ? user.socialAccounts.map((socialAccount) => ({ | ||
| provider: socialAccount.provider, | ||
| })) | ||
| : [], | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
guestOnlyMiddleware determines guest status solely from Authorization: Bearer header. This is consistent with frontend using bearer tokens. If you later support cookie/session auth for API calls, update this middleware to detect that too. For current app behavior no change required.
| function findByEmailCaseInsensitive(email, options = {}) { | ||
| return User.findOne({ | ||
| where: where(fn('lower', col('email')), String(email).trim().toLowerCase()), | ||
| ...options, | ||
| }); | ||
| } | ||
|
|
||
| function findByActivationToken(activationToken, options = {}) { | ||
| return User.findOne({ where: { activationToken }, ...options }); | ||
| } | ||
|
|
There was a problem hiding this comment.
socialAccount.model.js: indexes reference physical column names via field mappings (e.g., providerId, user_id). Ensure DB migrations/schema use the same column names so uniqueness indexes are applied correctly. This is informational — model is consistent as written.
| ## React Compiler | ||
|
|
There was a problem hiding this comment.
Add guestOnlyMiddleware to POST /register so only non-authenticated users can register. For example: authRouter.post('/register', guestOnlyMiddleware, authController.register); This is required by the task.
| ## React Compiler | ||
|
|
There was a problem hiding this comment.
Add guestOnlyMiddleware to POST /login so only non-authenticated users can log in. For example: authRouter.post('/login', guestOnlyMiddleware, authController.login); This is required by the task.
| # React + TypeScript + Vite | ||
|
|
||
| This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. | ||
|
|
||
| Currently, two official plugins are available: | ||
|
|
||
| - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) | ||
| - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) | ||
|
|
||
| ## React Compiler | ||
|
|
||
| The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). | ||
|
|
||
| ## Expanding the ESLint configuration | ||
|
|
||
| If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [ | ||
| // Other configs... | ||
|
|
||
| // Remove tseslint.configs.recommended and replace with this | ||
| tseslint.configs.recommendedTypeChecked, | ||
| // Alternatively, use this for stricter rules | ||
| tseslint.configs.strictTypeChecked, | ||
| // Optionally, add this for stylistic rules | ||
| tseslint.configs.stylisticTypeChecked, | ||
|
|
||
| // Other configs... | ||
| ], | ||
| languageOptions: { | ||
| parserOptions: { | ||
| project: ['./tsconfig.node.json', './tsconfig.app.json'], | ||
| tsconfigRootDir: import.meta.dirname, | ||
| }, | ||
| // other options... | ||
| }, | ||
| }, | ||
| ]) | ||
| ``` | ||
|
|
There was a problem hiding this comment.
There is no server-side logout route. The task requires logout to be available only for authenticated users and to redirect to login after logging out. Add a protected logout endpoint (use authMiddleware) that destroys server session (if used) and returns success so frontend can clear token and redirect to /login.
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [ |
There was a problem hiding this comment.
jwt.service.js relies on environment secrets such as JWT_ACCESS_SECRET. If these env vars are missing jwt.sign will throw. Validate required JWT secrets at startup or provide clear errors to avoid runtime failures in token operations (activation, reset, email-change).
|
|
||
| The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). | ||
|
|
||
| ## Expanding the ESLint configuration | ||
|
|
||
| If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), |
There was a problem hiding this comment.
mail.service.js throws when mail credentials are missing (GMAIL_USER / GMAIL_APP_PASSWORD). During development/tests this will break flows that send emails. Consider adding a development/noop transport or using an Ethereal account for local testing to avoid runtime exceptions.
|
|
||
| - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) | ||
| - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) | ||
|
|
||
| ## React Compiler | ||
|
|
||
| The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). | ||
|
|
||
| ## Expanding the ESLint configuration | ||
|
|
||
| If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [ | ||
| // Other configs... | ||
|
|
||
| // Remove tseslint.configs.recommended and replace with this | ||
| tseslint.configs.recommendedTypeChecked, | ||
| // Alternatively, use this for stricter rules | ||
| tseslint.configs.strictTypeChecked, | ||
| // Optionally, add this for stylistic rules | ||
| tseslint.configs.stylisticTypeChecked, |
There was a problem hiding this comment.
guestOnlyMiddleware and authMiddleware inspect Authorization Bearer header (JWT). This matches frontend api.ts that sets the header. If you later support cookie/session auth for API calls, update these middlewares accordingly. No change required now.
| { | ||
| files: ['**/*.{ts,tsx}'], |
There was a problem hiding this comment.
In auth.routes.js, POST /register is not protected by guestOnlyMiddleware. The task requires registration to be available only to non-authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/register', guestOnlyMiddleware, authController.register);
| { | ||
| files: ['**/*.{ts,tsx}'], |
There was a problem hiding this comment.
In auth.routes.js, POST /login is not protected by guestOnlyMiddleware. The task requires login to be available only to non-authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/login', guestOnlyMiddleware, authController.login);
| ecmaFeatures: { | ||
| jsx: true, | ||
| }, | ||
| }, | ||
| }, | ||
| plugins: { | ||
| 'react-hooks': reactHooks, |
There was a problem hiding this comment.
jwt.service.js relies on environment secrets (e.g., JWT_ACCESS_SECRET). If these are missing, jwt.sign will throw at runtime. Consider validating required JWT env vars at startup and providing clear error messages to avoid runtime failures across flows that depend on JWTs (activation, reset, email-change).
| files: ['**/*.{ts,tsx}'], | ||
| extends: [js.configs.recommended, ...tseslint.configs.recommended], | ||
| languageOptions: { | ||
| ecmaVersion: 2020, | ||
| globals: globals.browser, | ||
| parserOptions: { | ||
| ecmaFeatures: { | ||
| jsx: true, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
mail.service.js: getTransport throws when MAIL_FROM_ADDRESS/GMAIL_USER or GMAIL_APP_PASSWORD are missing. During development or tests this will break email-dependent flows (registration, password reset). Consider adding a development/noop transport (or Ethereal) to allow testing without real SMTP credentials.
| import { globalIgnores } from 'eslint/config'; | ||
|
|
||
| export default tseslint.config([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [js.configs.recommended, ...tseslint.configs.recommended], | ||
| languageOptions: { | ||
| ecmaVersion: 2020, | ||
| globals: globals.browser, | ||
| parserOptions: { | ||
| ecmaFeatures: { | ||
| jsx: true, | ||
| }, | ||
| }, | ||
| }, | ||
| plugins: { | ||
| 'react-hooks': reactHooks, | ||
| 'react-refresh': reactRefresh, | ||
| }, | ||
| rules: { | ||
| ...reactHooks.configs.recommended.rules, | ||
| 'react-refresh/only-export-components': [ | ||
| 'warn', | ||
| { allowConstantExport: true }, | ||
| ], |
There was a problem hiding this comment.
guestOnlyMiddleware and authMiddleware inspect only the Authorization header for a Bearer token, which matches the frontend api.ts behavior. If you later support cookie/session-based API auth, update these middlewares accordingly. (No change required now.)
| import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage'; | ||
| import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage'; |
There was a problem hiding this comment.
auth.routes.js: POST /register is not protected by guestOnlyMiddleware. The task requires registration to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/register', guestOnlyMiddleware, authController.register);
| import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage'; | ||
| import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage'; |
There was a problem hiding this comment.
auth.routes.js: POST /login is not protected by guestOnlyMiddleware. The task requires login to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/login', guestOnlyMiddleware, authController.login);
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Review Summary
I am requesting changes because a few checklist-level requirements are not yet enforced on the backend, even though the overall implementation is otherwise strong and feature-complete.
Critical issues that must be fixed (blockers):
- Guest-only protection on auth routes: In
src/backend/src/routes/auth.routes.js, the routesPOST /registerandPOST /login(around lines 9–11) are currently unguarded. The task explicitly requires that registration and login be available only to non‑authenticated users, so you need to insertguestOnlyMiddlewarebefore the controllers for both of these routes (e.g.authRouter.post('/register', guestOnlyMiddleware, authController.register)). Similarly,GET /activate/:hash(around line 38) must be available only to guests; wrap that route withguestOnlyMiddlewareas well. - Missing server-side logout endpoint: The requirements state that logout must be available only for authenticated users and redirect to the login page after logging out, but there is no backend logout route. Add an authenticated endpoint such as
POST /logoutinauth.routes.js, protect it withauthMiddleware, and in its controller destroy the server session (using your existing session helper orreq.session.destroy) so the frontend can call it, then clear the token and navigate to/login.
Important to verify (may also be a blocker if incomplete):
3. Email-change initiation behavior: user.controller.changeEmail delegates to userService.requestEmailChange; per the checklist, this flow must (a) verify the current password, (b) create an email-change token with jwtService.signEmailChangeToken, (c) send a confirmation link to the new email using mailService.sendEmailChangeConfirmation, and (d) notify the old email using mailService.sendEmailChangeNotification. Please ensure this service method exists and performs all these steps; if it’s missing or partial, implement the missing pieces.
Everything else—registration, activation, login, password reset, profile updates, and GitHub OAuth—is very well structured and aligns closely with the requirements. Once you add the guest-only guards, the logout route, and confirm the email-change initiation logic, your solution will fully meet the task’s standards.
✨ 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
| function authMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { | ||
| return next(ApiError.unauthorized()); | ||
| } | ||
|
|
||
| const payload = jwtService.verify(token); | ||
|
|
||
| if (!payload) { | ||
| return next(ApiError.unauthorized()); | ||
| } | ||
|
|
||
| req.user = payload; | ||
| next(); |
There was a problem hiding this comment.
authMiddleware expects an Authorization header with Bearer token. However many frontend requests use access token stored in localStorage and set Authorization header in api.request; this is consistent. Ensure any requests relying on cookie-based session authentication use appropriate middleware — currently API uses bearer JWT, not session auth.
|
|
||
| function guestOnlyMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
|
|
||
| if (!header) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { |
There was a problem hiding this comment.
The middleware correctly returns ApiError for ApiError instances and logs other errors. However, it exposes full error via process.stderr.write(${error}\n), which is fine for logs but ensure sensitive info isn't logged in production. (Informational)
| function guestOnlyMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
|
|
||
| if (!header) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const payload = jwtService.verify(token); | ||
|
|
||
| if (!payload) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| next(ApiError.badRequest('This action is only available when signed out')); |
There was a problem hiding this comment.
guestOnlyMiddleware inspects Authorization header to determine signed-in state. This approach works but relies solely on Authorization header; if the client uses cookie-based sessions, guestOnlyMiddleware won't detect them. Given the app uses JWT bearer tokens in API calls, this is acceptable. Consider documenting that guestOnly applies to Bearer-authenticated requests only.
| function guestOnlyMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
|
|
||
| if (!header) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const payload = jwtService.verify(token); | ||
|
|
||
| if (!payload) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| next(ApiError.badRequest('This action is only available when signed out')); |
There was a problem hiding this comment.
In guestOnlyMiddleware, the middleware calls next() in multiple branches and finally next(ApiError.badRequest(...)). Behavior is correct, but you may simplify flow for clarity. No functional change required.
|
|
||
| function getPrimaryEmail(profile) { |
There was a problem hiding this comment.
In auth.routes.js POST /register and /login are not protected by guestOnlyMiddleware. The task requires registration and login to be available only to non‑authenticated users. Add guestOnlyMiddleware to these routes (e.g., authRouter.post('/register', guestOnlyMiddleware, authController.register)).
| 'use strict'; | ||
|
|
||
| const { sequelize } = require('../db'); | ||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const jwtService = require('./jwt.service'); | ||
| const userService = require('./user.service'); | ||
| const { GITHUB_PROVIDER } = require('./socialAccount.service'); | ||
|
|
||
| function getPrimaryEmail(profile) { | ||
| const email = profile.emails.find(Boolean) || ''; | ||
|
|
||
| return String(email).trim(); | ||
| } | ||
|
|
||
| function getDisplayName(profile, email) { | ||
| const fallbackName = email.split('@')[0] || 'GitHub user'; | ||
|
|
||
| return String(profile.displayName || profile.username || fallbackName).trim(); | ||
| } | ||
|
|
||
| async function finalizeGitHubAuthentication(profile, linkUserId = null) { | ||
| const providerUserId = String(profile.providerUserId || '').trim(); | ||
|
|
||
| if (!providerUserId) { | ||
| throw ApiError.badRequest('GitHub did not return a valid user id.'); | ||
| } | ||
|
|
||
| const email = getPrimaryEmail(profile); | ||
|
|
||
| if (!email) { | ||
| throw ApiError.badRequest('Error'); | ||
| } | ||
|
|
||
| const name = getDisplayName(profile, email); | ||
|
|
||
| return sequelize.transaction(async (transaction) => { | ||
| const existingSocialAccount = await SocialAccount.findOne({ | ||
| where: { | ||
| provider: GITHUB_PROVIDER, | ||
| providerUserId, | ||
| }, | ||
| transaction, | ||
| }); |
There was a problem hiding this comment.
There is no server-side logout route. The task requires logout be available only for authenticated users and redirect to login after logging out. Add a logout endpoint (protected by authMiddleware) that destroys session (if used) and/or advises frontend to clear token, then returns 204 or a JSON response. Frontend should call it and redirect to /login.
| 'use strict'; | ||
|
|
||
| const { sequelize } = require('../db'); | ||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const jwtService = require('./jwt.service'); | ||
| const userService = require('./user.service'); | ||
| const { GITHUB_PROVIDER } = require('./socialAccount.service'); | ||
|
|
||
| function getPrimaryEmail(profile) { | ||
| const email = profile.emails.find(Boolean) || ''; | ||
|
|
||
| return String(email).trim(); | ||
| } | ||
|
|
||
| function getDisplayName(profile, email) { | ||
| const fallbackName = email.split('@')[0] || 'GitHub user'; | ||
|
|
||
| return String(profile.displayName || profile.username || fallbackName).trim(); | ||
| } |
There was a problem hiding this comment.
guestOnlyMiddleware and authMiddleware inspect only the Authorization header (Bearer token). This matches the frontend which sets the header from localStorage, but the GitHub OAuth flows rely on express-session cookies. Ensure CORS and session cookie settings allow the session cookie across the OAuth redirect (sameSite, secure and credentials settings). This is operational guidance to avoid losing OAuth flow state.
| }; | ||
| } | ||
|
|
||
| let user = await userService.findByEmailCaseInsensitive(email, { | ||
| transaction, | ||
| }); | ||
|
|
||
| if (!user) { | ||
| user = await User.create( | ||
| { | ||
| name, | ||
| email, |
There was a problem hiding this comment.
saveSession in auth.controller.js calls req.session.save without checking if req.session exists. Given express-session is configured this should be fine, but add a guard if (!req.session) return Promise.resolve(); for extra robustness.
| if (!providerUserId) { | ||
| throw ApiError.badRequest('GitHub did not return a valid user id.'); | ||
| } | ||
|
|
||
| const email = getPrimaryEmail(profile); | ||
|
|
||
| if (!email) { | ||
| throw ApiError.badRequest('Error'); | ||
| } | ||
|
|
||
| const name = getDisplayName(profile, email); | ||
|
|
||
| return sequelize.transaction(async (transaction) => { | ||
| const existingSocialAccount = await SocialAccount.findOne({ | ||
| where: { | ||
| provider: GITHUB_PROVIDER, | ||
| providerUserId, | ||
| }, | ||
| transaction, | ||
| }); | ||
|
|
||
| if (linkUserId) { | ||
| const linkedUser = await userService.findById(linkUserId, { | ||
| transaction, | ||
| }); | ||
|
|
||
| if (!linkedUser) { | ||
| throw ApiError.notFound('User not found'); | ||
| } | ||
|
|
||
| if (existingSocialAccount) { | ||
| if (existingSocialAccount.userId !== linkedUser.id) { | ||
| throw ApiError.badRequest( | ||
| 'This GitHub account is already linked to another user.', | ||
| ); | ||
| } | ||
|
|
||
| return { | ||
| accessToken: null, | ||
| message: 'GitHub is already connected to your account.', | ||
| redirectPath: '/account', | ||
| }; | ||
| } | ||
|
|
||
| await SocialAccount.create( | ||
| { | ||
| provider: GITHUB_PROVIDER, | ||
| providerUserId, | ||
| providerEmail: email, | ||
| userId: linkedUser.id, | ||
| }, | ||
| { transaction }, | ||
| ); | ||
|
|
||
| return { | ||
| accessToken: null, | ||
| message: 'GitHub account connected successfully.', | ||
| redirectPath: '/account', | ||
| }; | ||
| } | ||
|
|
||
| if (existingSocialAccount) { | ||
| const existingUser = await userService.findById( | ||
| existingSocialAccount.userId, | ||
| { | ||
| transaction, | ||
| }, |
There was a problem hiding this comment.
Activation and email-change redirects include the access token in the URL query string. This works with the frontend ActivationPage which reads and stores the token, but exposing tokens in query params can leak them via referer or browser history. You already issue short-lived tokens; consider documenting this behavior or switching to a fragment/hash or POST if stricter security is desired.
| const { | ||
| validatePassword, |
There was a problem hiding this comment.
auth.routes.js: POST /register and POST /login are not using guestOnlyMiddleware. The task requires registration and login to be available only for non‑authenticated users — add guestOnlyMiddleware to these routes (e.g., authRouter.post('/register', guestOnlyMiddleware, authController.register)).
| /* eslint-disable indent */ | ||
| 'use strict'; | ||
|
|
||
| const bcrypt = require('bcryptjs'); | ||
| const { col, fn, where } = require('sequelize'); | ||
|
|
||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const { | ||
| validatePassword, | ||
| validateName, | ||
| validateEmail, | ||
| } = require('../utils/validate'); | ||
| const jwtService = require('./jwt.service'); | ||
| const mailService = require('./mail.service'); | ||
|
|
||
| function normalize(user) { | ||
| return { | ||
| id: user.id, | ||
| name: user.name, | ||
| email: user.email, | ||
| hasPassword: Boolean(user.password), | ||
| socialAccounts: Array.isArray(user.socialAccounts) | ||
| ? user.socialAccounts.map((socialAccount) => ({ | ||
| provider: socialAccount.provider, | ||
| })) | ||
| : [], | ||
| }; | ||
| } | ||
|
|
||
| function findByEmail(email, options = {}) { | ||
| return User.findOne({ where: { email }, ...options }); | ||
| } | ||
|
|
||
| function findByEmailCaseInsensitive(email, options = {}) { | ||
| return User.findOne({ | ||
| where: where(fn('lower', col('email')), String(email).trim().toLowerCase()), | ||
| ...options, | ||
| }); | ||
| } | ||
|
|
||
| function findByActivationToken(activationToken, options = {}) { | ||
| return User.findOne({ where: { activationToken }, ...options }); | ||
| } |
There was a problem hiding this comment.
No logout route exists in auth.routes.js or controllers. The task requires logout only for authenticated users and redirect to login after logging out. Add a protected logout endpoint (use authMiddleware) that destroys server session (if used) and returns success so frontend can clear token and navigate to /login.
| const nameError = validateName(newName); | ||
|
|
||
| if (nameError) { | ||
| throw ApiError.badRequest('Validation failed', { newName: nameError }); | ||
| } | ||
|
|
||
| const user = await findById(userId); | ||
|
|
||
| if (!user) { | ||
| throw ApiError.notFound('User not found'); | ||
| } | ||
|
|
There was a problem hiding this comment.
saveSession in auth.controller.js calls req.session.save without guarding for missing req.session. Express-session is configured, but for robustness consider adding if (!req.session) return Promise.resolve(); at start of saveSession to avoid potential runtime errors.
| throw ApiError.badRequest( | ||
| 'Set a password before changing your email address.', | ||
| { | ||
| password: 'Set a password before changing your email address.', | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| const errors = {}; | ||
| const normalizedEmail = String(newEmail).trim(); | ||
|
|
||
| if (!password) { | ||
| errors.password = 'Password is required'; | ||
| } | ||
|
|
||
| const emailError = validateEmail(normalizedEmail); | ||
|
|
There was a problem hiding this comment.
GitHub OAuth flows store intent in req.session.githubAuth. Ensure express-session cookie settings and CORS (credentials: true) permit the session cookie to be sent across OAuth redirects; otherwise session state may be lost on callback and flow will fallback. Verify sameSite/secure in production.
|
|
||
| function normalize(user) { | ||
| return { | ||
| id: user.id, | ||
| name: user.name, | ||
| email: user.email, | ||
| hasPassword: Boolean(user.password), |
There was a problem hiding this comment.
jwt.service.js: signing functions use environment variables directly (e.g., process.env.JWT_ACCESS_SECRET). If these are undefined, jwt.sign will throw. Ensure required JWT secrets are set in env for production. Consider validating presence at startup or providing clearer errors.
| validatePassword, | ||
| validateName, | ||
| validateEmail, | ||
| } = require('../utils/validate'); | ||
| const jwtService = require('./jwt.service'); | ||
| const mailService = require('./mail.service'); | ||
|
|
||
| function normalize(user) { | ||
| return { |
There was a problem hiding this comment.
mail.service.js: getTransport throws if GMAIL_USER / GMAIL_APP_PASSWORD missing. For development/testing consider a noop/mock transport to avoid runtime failures when sending emails is not configured.
|
|
||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const { | ||
| validatePassword, | ||
| validateName, | ||
| validateEmail, | ||
| } = require('../utils/validate'); | ||
| const jwtService = require('./jwt.service'); | ||
| const mailService = require('./mail.service'); | ||
|
|
||
| function normalize(user) { | ||
| return { | ||
| id: user.id, | ||
| name: user.name, | ||
| email: user.email, | ||
| hasPassword: Boolean(user.password), | ||
| socialAccounts: Array.isArray(user.socialAccounts) | ||
| ? user.socialAccounts.map((socialAccount) => ({ | ||
| provider: socialAccount.provider, | ||
| })) | ||
| : [], | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
guestOnlyMiddleware determines guest status solely from Authorization: Bearer header. This is consistent with frontend using bearer tokens. If you later support cookie/session auth for API calls, update this middleware to detect that too. For current app behavior no change required.
| function findByEmailCaseInsensitive(email, options = {}) { | ||
| return User.findOne({ | ||
| where: where(fn('lower', col('email')), String(email).trim().toLowerCase()), | ||
| ...options, | ||
| }); | ||
| } | ||
|
|
||
| function findByActivationToken(activationToken, options = {}) { | ||
| return User.findOne({ where: { activationToken }, ...options }); | ||
| } | ||
|
|
There was a problem hiding this comment.
socialAccount.model.js: indexes reference physical column names via field mappings (e.g., providerId, user_id). Ensure DB migrations/schema use the same column names so uniqueness indexes are applied correctly. This is informational — model is consistent as written.
| ## React Compiler | ||
|
|
There was a problem hiding this comment.
Add guestOnlyMiddleware to POST /register so only non-authenticated users can register. For example: authRouter.post('/register', guestOnlyMiddleware, authController.register); This is required by the task.
| ## React Compiler | ||
|
|
There was a problem hiding this comment.
Add guestOnlyMiddleware to POST /login so only non-authenticated users can log in. For example: authRouter.post('/login', guestOnlyMiddleware, authController.login); This is required by the task.
| # React + TypeScript + Vite | ||
|
|
||
| This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. | ||
|
|
||
| Currently, two official plugins are available: | ||
|
|
||
| - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) | ||
| - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) | ||
|
|
||
| ## React Compiler | ||
|
|
||
| The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). | ||
|
|
||
| ## Expanding the ESLint configuration | ||
|
|
||
| If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [ | ||
| // Other configs... | ||
|
|
||
| // Remove tseslint.configs.recommended and replace with this | ||
| tseslint.configs.recommendedTypeChecked, | ||
| // Alternatively, use this for stricter rules | ||
| tseslint.configs.strictTypeChecked, | ||
| // Optionally, add this for stylistic rules | ||
| tseslint.configs.stylisticTypeChecked, | ||
|
|
||
| // Other configs... | ||
| ], | ||
| languageOptions: { | ||
| parserOptions: { | ||
| project: ['./tsconfig.node.json', './tsconfig.app.json'], | ||
| tsconfigRootDir: import.meta.dirname, | ||
| }, | ||
| // other options... | ||
| }, | ||
| }, | ||
| ]) | ||
| ``` | ||
|
|
There was a problem hiding this comment.
There is no server-side logout route. The task requires logout to be available only for authenticated users and to redirect to login after logging out. Add a protected logout endpoint (use authMiddleware) that destroys server session (if used) and returns success so frontend can clear token and redirect to /login.
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [ |
There was a problem hiding this comment.
jwt.service.js relies on environment secrets such as JWT_ACCESS_SECRET. If these env vars are missing jwt.sign will throw. Validate required JWT secrets at startup or provide clear errors to avoid runtime failures in token operations (activation, reset, email-change).
|
|
||
| The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). | ||
|
|
||
| ## Expanding the ESLint configuration | ||
|
|
||
| If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), |
There was a problem hiding this comment.
mail.service.js throws when mail credentials are missing (GMAIL_USER / GMAIL_APP_PASSWORD). During development/tests this will break flows that send emails. Consider adding a development/noop transport or using an Ethereal account for local testing to avoid runtime exceptions.
|
|
||
| - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) | ||
| - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) | ||
|
|
||
| ## React Compiler | ||
|
|
||
| The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). | ||
|
|
||
| ## Expanding the ESLint configuration | ||
|
|
||
| If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [ | ||
| // Other configs... | ||
|
|
||
| // Remove tseslint.configs.recommended and replace with this | ||
| tseslint.configs.recommendedTypeChecked, | ||
| // Alternatively, use this for stricter rules | ||
| tseslint.configs.strictTypeChecked, | ||
| // Optionally, add this for stylistic rules | ||
| tseslint.configs.stylisticTypeChecked, |
There was a problem hiding this comment.
guestOnlyMiddleware and authMiddleware inspect Authorization Bearer header (JWT). This matches frontend api.ts that sets the header. If you later support cookie/session auth for API calls, update these middlewares accordingly. No change required now.
| { | ||
| files: ['**/*.{ts,tsx}'], |
There was a problem hiding this comment.
In auth.routes.js, POST /register is not protected by guestOnlyMiddleware. The task requires registration to be available only to non-authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/register', guestOnlyMiddleware, authController.register);
| { | ||
| files: ['**/*.{ts,tsx}'], |
There was a problem hiding this comment.
In auth.routes.js, POST /login is not protected by guestOnlyMiddleware. The task requires login to be available only to non-authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/login', guestOnlyMiddleware, authController.login);
| ecmaFeatures: { | ||
| jsx: true, | ||
| }, | ||
| }, | ||
| }, | ||
| plugins: { | ||
| 'react-hooks': reactHooks, |
There was a problem hiding this comment.
jwt.service.js relies on environment secrets (e.g., JWT_ACCESS_SECRET). If these are missing, jwt.sign will throw at runtime. Consider validating required JWT env vars at startup and providing clear error messages to avoid runtime failures across flows that depend on JWTs (activation, reset, email-change).
| files: ['**/*.{ts,tsx}'], | ||
| extends: [js.configs.recommended, ...tseslint.configs.recommended], | ||
| languageOptions: { | ||
| ecmaVersion: 2020, | ||
| globals: globals.browser, | ||
| parserOptions: { | ||
| ecmaFeatures: { | ||
| jsx: true, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
mail.service.js: getTransport throws when MAIL_FROM_ADDRESS/GMAIL_USER or GMAIL_APP_PASSWORD are missing. During development or tests this will break email-dependent flows (registration, password reset). Consider adding a development/noop transport (or Ethereal) to allow testing without real SMTP credentials.
| import { globalIgnores } from 'eslint/config'; | ||
|
|
||
| export default tseslint.config([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [js.configs.recommended, ...tseslint.configs.recommended], | ||
| languageOptions: { | ||
| ecmaVersion: 2020, | ||
| globals: globals.browser, | ||
| parserOptions: { | ||
| ecmaFeatures: { | ||
| jsx: true, | ||
| }, | ||
| }, | ||
| }, | ||
| plugins: { | ||
| 'react-hooks': reactHooks, | ||
| 'react-refresh': reactRefresh, | ||
| }, | ||
| rules: { | ||
| ...reactHooks.configs.recommended.rules, | ||
| 'react-refresh/only-export-components': [ | ||
| 'warn', | ||
| { allowConstantExport: true }, | ||
| ], |
There was a problem hiding this comment.
guestOnlyMiddleware and authMiddleware inspect only the Authorization header for a Bearer token, which matches the frontend api.ts behavior. If you later support cookie/session-based API auth, update these middlewares accordingly. (No change required now.)
| import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage'; | ||
| import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage'; |
There was a problem hiding this comment.
auth.routes.js: POST /register is not protected by guestOnlyMiddleware. The task requires registration to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/register', guestOnlyMiddleware, authController.register);
| import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage'; | ||
| import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage'; |
There was a problem hiding this comment.
auth.routes.js: POST /login is not protected by guestOnlyMiddleware. The task requires login to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/login', guestOnlyMiddleware, authController.login);
| import { useEffect } from 'react'; | ||
| import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'; | ||
| import { LoginPage } from './pages/LoginPage'; | ||
| import { RegisterPage } from './pages/RegisterPage'; | ||
| import { ActivationPage } from './pages/ActivationPage'; | ||
| import { AccountPage } from './pages/AccountPage'; | ||
| import { NotFoundPage } from './pages/NotFoundPage'; | ||
| import { ResetPasswordRequestPage } from './pages/ResetPasswordRequestPage'; | ||
| import { ResetPasswordEmailSentPage } from './pages/ResetPasswordEmailSentPage'; | ||
| import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage'; | ||
| import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage'; | ||
| import './styles.css'; | ||
|
|
||
| const pageTitles: Record<string, string> = { | ||
| '/': 'Log In', | ||
| '/login': 'Log In', | ||
| '/register': 'Sign Up', | ||
| '/reset-password': 'Reset Password', | ||
| '/reset-password/sent': 'Check Your Email', | ||
| '/reset-password/confirm': 'Choose New Password', | ||
| '/reset-password/success': 'Password Reset Complete', | ||
| '/activation': 'Email Verified', | ||
| '/account': 'My Account', | ||
| }; | ||
|
|
||
| function PageTitle() { | ||
| const { pathname } = useLocation(); | ||
|
|
||
| useEffect(() => { | ||
| document.title = pageTitles[pathname] ?? 'Page Not Found'; | ||
| }, [pathname]); | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| function App() { | ||
| return ( | ||
| <BrowserRouter> | ||
| <PageTitle /> | ||
| <Routes> | ||
| <Route path="/" element={<Navigate to="/login" replace />} /> | ||
| <Route path="/login" element={<LoginPage />} /> | ||
| <Route path="/register" element={<RegisterPage />} /> | ||
| <Route path="/reset-password" element={<ResetPasswordRequestPage />} /> | ||
| <Route |
There was a problem hiding this comment.
There is no server-side logout route in the backend. The task requires logout to be available only for authenticated users and to redirect to login after logging out. Add a protected logout endpoint (use authMiddleware) that destroys the server session (if any) and returns success so the frontend can clear token and navigate to /login.
| '/register': 'Sign Up', | ||
| '/reset-password': 'Reset Password', | ||
| '/reset-password/sent': 'Check Your Email', | ||
| '/reset-password/confirm': 'Choose New Password', | ||
| '/reset-password/success': 'Password Reset Complete', | ||
| '/activation': 'Email Verified', | ||
| '/account': 'My Account', |
There was a problem hiding this comment.
jwt.service.js relies on environment secrets (e.g., JWT_ACCESS_SECRET). If these are missing, jwt.sign will throw. Consider validating required JWT env vars at startup and providing clear errors to avoid runtime failures in flows that depend on JWTs (activation, password-reset, email-change).
| import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage'; | ||
| import './styles.css'; | ||
|
|
||
| const pageTitles: Record<string, string> = { | ||
| '/': 'Log In', | ||
| '/login': 'Log In', | ||
| '/register': 'Sign Up', | ||
| '/reset-password': 'Reset Password', | ||
| '/reset-password/sent': 'Check Your Email', | ||
| '/reset-password/confirm': 'Choose New Password', |
There was a problem hiding this comment.
mail.service.js: getTransport throws when MAIL_FROM_ADDRESS/GMAIL_USER or GMAIL_APP_PASSWORD are missing. During development/tests this will break email-dependent flows (registration, password reset). Consider adding a development/noop transport (or Ethereal) to allow testing without real SMTP credentials.
|
|
||
| useEffect(() => { |
There was a problem hiding this comment.
In src/backend/src/routes/auth.routes.js the POST /register route is not protected by guestOnlyMiddleware. The task requires registration to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/register', guestOnlyMiddleware, authController.register);
|
|
||
| useEffect(() => { |
There was a problem hiding this comment.
In src/backend/src/routes/auth.routes.js the POST /login route is not protected by guestOnlyMiddleware. The task requires login to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/login', guestOnlyMiddleware, authController.login);
|
|
||
| if (token) { | ||
| setAccessToken(token); | ||
| } | ||
|
|
||
| const timer = setTimeout(() => { | ||
| navigate('/account', { replace: true }); |
There was a problem hiding this comment.
src/backend/src/services/jwt.service.js relies on environment JWT secrets (e.g., JWT_ACCESS_SECRET). If these env vars are missing jwt.sign will throw. Consider validating required JWT env vars at startup and providing clear errors to avoid runtime failures in flows that use tokens (activation, password-reset, email-change).
| useEffect(() => { | ||
| document.title = isEmailChange ? 'Email Changed' : 'Email Verified'; | ||
| }, [isEmailChange]); | ||
|
|
||
| useEffect(() => { | ||
| const token = searchParams.get('accessToken'); | ||
|
|
||
| if (token) { | ||
| setAccessToken(token); | ||
| } |
There was a problem hiding this comment.
src/backend/src/services/mail.service.js throws when MAIL_FROM_ADDRESS/GMAIL_USER or GMAIL_APP_PASSWORD are missing. During development/tests this will break registration/password-reset flows that send emails. Consider adding a development/noop transport (or Ethereal) or handling missing creds gracefully in non-production to allow testing without real SMTP credentials.
|
|
||
| return <RegisterForm />; |
There was a problem hiding this comment.
In src/backend/src/routes/auth.routes.js the POST /register route is not protected by guestOnlyMiddleware. The task requires registration to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/register', guestOnlyMiddleware, authController.register);
|
|
||
| return <RegisterForm />; |
There was a problem hiding this comment.
In src/backend/src/routes/auth.routes.js the POST /login route is not protected by guestOnlyMiddleware. The task requires login to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/login', guestOnlyMiddleware, authController.login);
| @@ -0,0 +1,2 @@ | |||
| node_modules | |||
There was a problem hiding this comment.
Password reset endpoints (/password-reset and /password-reset/confirm) are correctly using guestOnlyMiddleware in auth.routes.js, but ensure the activation and register/login routes use the same middleware for consistency with requirements.
| function isGithubAuthEnabled() { | ||
| return Boolean( | ||
| process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET, | ||
| ); | ||
| } |
There was a problem hiding this comment.
In passport.js lines 6-9: The isGithubAuthEnabled function checks process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET but returns Boolean(...,). The trailing comma is harmless in JS but the overall check is fine. No action required.
|
|
||
| res.json({ message: 'Name changed successfully.' }); | ||
| } catch (error) { | ||
| next(error); |
There was a problem hiding this comment.
Register and login routes are unprotected. According to the task, these must be available only to non-authenticated users. In src/backend/src/routes/auth.routes.js add guestOnlyMiddleware to POST /register and POST /login so only guests can access them.
| } | ||
|
|
||
| async function removeGithubAccount(req, res, next) { | ||
| try { |
There was a problem hiding this comment.
The activation endpoint GET /activate/:hash should be available only to non-authenticated users as per the requirements. In src/backend/src/routes/auth.routes.js wrap the /activate/:hash route with guestOnlyMiddleware.
|
|
||
| await userService.changeName({ userId: req.user.id, newName }); | ||
|
|
||
| res.json({ message: 'Name changed successfully.' }); |
There was a problem hiding this comment.
The app initializes passport with passport.initialize() but does not call passport.session(). You store GitHub flow state in req.session and use passport.authenticate with session:false, so this may be fine. If you intend to use Passport session deserialization later, consider adding app.use(passport.session()) in src/backend/src/app.js.
| await userService.requestEmailChange({ | ||
| userId: req.user.id, | ||
| password, | ||
| newEmail, | ||
| }); | ||
|
|
||
| res.json({ | ||
| message: 'Confirmation link sent to your new email address.', | ||
| }); |
There was a problem hiding this comment.
UserController.changeEmail (src/backend/src/controllers/user.controller.js) calls userService.requestEmailChange. Ensure the implementation of userService.requestEmailChange validates the current password, creates an email-change token (using jwtService.signEmailChangeToken), sends confirmation to the new email (mailService.sendEmailChangeConfirmation) and notifies the old email (mailService.sendEmailChangeNotification). This is required by the checklist item for changing email.
| next(error); | ||
| } |
There was a problem hiding this comment.
There is no logout endpoint in the provided auth routes. The requirements state "The application must allow a user to log out (only authenticated) and redirect to login after logging out." Add an authenticated POST /logout route (use authMiddleware) that destroys the session (use the existing destroySession logic) and responds so the client can redirect to /login.
| unique: true, | ||
| fields: ['user_id', 'provider'], | ||
| }, | ||
| ], | ||
| }, | ||
| ); | ||
|
|
||
| User.hasMany(SocialAccount, { | ||
| foreignKey: 'userId', | ||
| as: 'socialAccounts', | ||
| }); | ||
|
|
||
| SocialAccount.belongsTo(User, { |
There was a problem hiding this comment.
The controller relies on userService.requestEmailChange to implement the email-change initiation (require current password, send confirmation to new email, notify old email). Ensure that user.service.requestEmailChange exists and performs these steps; otherwise this violates the requirement: "To change an email on the Profile page, the user should type the password, confirm the new email and notify the old email about the change."
|
|
||
| const SocialAccount = sequelize.define( | ||
| 'SocialAccount', | ||
| { | ||
| id: { |
There was a problem hiding this comment.
me, changePassword, changeName, changeEmail and removeGithubAccount all forward errors via next(error). Ensure authMiddleware protects these routes (it is applied in user.routes.js) so they are available only to authenticated users as required.
| const { | ||
| validateName, |
There was a problem hiding this comment.
auth.routes.js: POST /register is not protected by guestOnlyMiddleware. The task requires registration to be available only to non-authenticated users — add guestOnlyMiddleware to this route.
| const { User } = require('../models/user.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const { | ||
| validateName, |
There was a problem hiding this comment.
auth.routes.js: POST /login is not protected by guestOnlyMiddleware. The task requires login to be available only to non-authenticated users — add guestOnlyMiddleware to this route.
| } | ||
|
|
||
| if (emailError) { | ||
| errors.email = emailError; |
There was a problem hiding this comment.
auth.routes.js: GET /activate/:hash is currently public. The task requires the activation page to be available only to non-authenticated users — wrap this route with guestOnlyMiddleware.
|
|
||
| if (Object.keys(errors).length > 0) { | ||
| throw ApiError.badRequest('Validation failed', errors); | ||
| } | ||
|
|
||
| const existing = await userService.findByEmail(email); | ||
|
|
||
| if (existing) { | ||
| throw ApiError.badRequest('Validation failed', { | ||
| email: 'Email is already taken', | ||
| }); | ||
| } | ||
|
|
There was a problem hiding this comment.
user.controller.js: changeEmail delegates to userService.requestEmailChange. Ensure that userService.requestEmailChange validates the provided password, generates an email-change confirmation token, sends the confirmation to the new email, and notifies the old email (mailService.sendEmailChangeConfirmation and sendEmailChangeNotification). If missing, implement these steps to satisfy the checklist.
| } | ||
|
|
||
| if (emailError) { | ||
| errors.email = emailError; |
There was a problem hiding this comment.
app.js: passport.initialize() is used but passport.session() is not mounted. Since you use express-session to store githubAuth state and use passport.authenticate with session:false, this may be intentional. If you require Passport session deserialization later, add app.use(passport.session()). Verify intended behavior for GitHub link/auth flows.
| userRouter.patch('/me/password', authMiddleware, userController.changePassword); | ||
| userRouter.patch('/me/name', authMiddleware, userController.changeName); |
There was a problem hiding this comment.
Per the task, registration must be available only to non-authenticated users. In src/backend/src/routes/auth.routes.js the POST /register route is not protected by guestOnlyMiddleware — add guestOnlyMiddleware to this route.
|
|
||
| userRouter.get('/me', authMiddleware, userController.me); | ||
| userRouter.patch('/me/password', authMiddleware, userController.changePassword); | ||
| userRouter.patch('/me/name', authMiddleware, userController.changeName); |
There was a problem hiding this comment.
Per the task, login must be available only to non-authenticated users. In src/backend/src/routes/auth.routes.js the POST /login route is not protected by guestOnlyMiddleware — add guestOnlyMiddleware to this route.
| type: DataTypes.INTEGER, | ||
| autoIncrement: true, |
There was a problem hiding this comment.
POST /register must be available only to non-authenticated users per the task. In src/backend/src/routes/auth.routes.js the route at line 10 registers '/register' without guestOnlyMiddleware. Add guestOnlyMiddleware to this route.
| { | ||
| id: { | ||
| type: DataTypes.INTEGER, | ||
| autoIncrement: true, |
There was a problem hiding this comment.
POST /login must be available only to non-authenticated users per the task. In src/backend/src/routes/auth.routes.js the route at line 11 registers '/login' without guestOnlyMiddleware. Add guestOnlyMiddleware to this route.
|
|
||
| authRouter.post('/register', authController.register); | ||
| authRouter.post('/login', authController.login); |
There was a problem hiding this comment.
In auth.routes.js lines 9-11 the POST /register and POST /login routes are unguarded. The task requires these be available only to non-authenticated users — add guestOnlyMiddleware to both routes.
| guestOnlyMiddleware, | ||
| authController.resetPassword, | ||
| ); | ||
| authRouter.get('/activate/:hash', authController.activate); |
There was a problem hiding this comment.
In auth.routes.js line 38 the GET /activate/:hash route is public. The task requires the activation page to be available only to non-authenticated users — wrap this route with guestOnlyMiddleware.
| authRouter.get( | ||
| '/email-change/confirm/:token', | ||
| authController.confirmEmailChange, | ||
| ); | ||
|
|
||
| module.exports = { authRouter }; |
There was a problem hiding this comment.
There is no logout endpoint in the provided auth routes. The task requires a logout action available only to authenticated users and redirect to login after logging out. Add an authenticated route (e.g. POST /logout) that calls session destroy (use the destroySession helper or req.session.destroy) and returns success so the client can redirect.
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Review Summary
I am requesting changes because a few checklist-level requirements are not yet enforced on the backend, even though the overall implementation is otherwise strong and feature-complete.
Critical issues that must be fixed (blockers):
- Guest-only protection on auth routes: In
src/backend/src/routes/auth.routes.js, the routesPOST /registerandPOST /login(around lines 9–11) are currently unguarded. The task explicitly requires that registration and login be available only to non‑authenticated users, so you need to insertguestOnlyMiddlewarebefore the controllers for both of these routes (e.g.authRouter.post('/register', guestOnlyMiddleware, authController.register)). Similarly,GET /activate/:hash(around line 38) must be available only to guests; wrap that route withguestOnlyMiddlewareas well. - Missing server-side logout endpoint: The requirements state that logout must be available only for authenticated users and redirect to the login page after logging out, but there is no backend logout route. Add an authenticated endpoint such as
POST /logoutinauth.routes.js, protect it withauthMiddleware, and in its controller destroy the server session (using your existing session helper orreq.session.destroy) so the frontend can call it, then clear the token and navigate to/login.
Important to verify (may also be a blocker if incomplete):
3. Email-change initiation behavior: user.controller.changeEmail delegates to userService.requestEmailChange; per the checklist, this flow must (a) verify the current password, (b) create an email-change token with jwtService.signEmailChangeToken, (c) send a confirmation link to the new email using mailService.sendEmailChangeConfirmation, and (d) notify the old email using mailService.sendEmailChangeNotification. Please ensure this service method exists and performs all these steps; if it’s missing or partial, implement the missing pieces.
Everything else—registration, activation, login, password reset, profile updates, and GitHub OAuth—is very well structured and aligns closely with the requirements. Once you add the guest-only guards, the logout route, and confirm the email-change initiation logic, your solution will fully meet the task’s standards.
✨ 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
| function authMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { | ||
| return next(ApiError.unauthorized()); | ||
| } | ||
|
|
||
| const payload = jwtService.verify(token); | ||
|
|
||
| if (!payload) { | ||
| return next(ApiError.unauthorized()); | ||
| } | ||
|
|
||
| req.user = payload; | ||
| next(); |
There was a problem hiding this comment.
authMiddleware expects an Authorization header with Bearer token. However many frontend requests use access token stored in localStorage and set Authorization header in api.request; this is consistent. Ensure any requests relying on cookie-based session authentication use appropriate middleware — currently API uses bearer JWT, not session auth.
|
|
||
| function guestOnlyMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
|
|
||
| if (!header) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { |
There was a problem hiding this comment.
The middleware correctly returns ApiError for ApiError instances and logs other errors. However, it exposes full error via process.stderr.write(${error}\n), which is fine for logs but ensure sensitive info isn't logged in production. (Informational)
| function guestOnlyMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
|
|
||
| if (!header) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const payload = jwtService.verify(token); | ||
|
|
||
| if (!payload) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| next(ApiError.badRequest('This action is only available when signed out')); |
There was a problem hiding this comment.
guestOnlyMiddleware inspects Authorization header to determine signed-in state. This approach works but relies solely on Authorization header; if the client uses cookie-based sessions, guestOnlyMiddleware won't detect them. Given the app uses JWT bearer tokens in API calls, this is acceptable. Consider documenting that guestOnly applies to Bearer-authenticated requests only.
| function guestOnlyMiddleware(req, res, next) { | ||
| const header = req.headers.authorization || ''; | ||
|
|
||
| if (!header) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const [type, token] = header.split(' '); | ||
|
|
||
| if (type !== 'Bearer' || !token) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const payload = jwtService.verify(token); | ||
|
|
||
| if (!payload) { | ||
| next(); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| next(ApiError.badRequest('This action is only available when signed out')); |
There was a problem hiding this comment.
In guestOnlyMiddleware, the middleware calls next() in multiple branches and finally next(ApiError.badRequest(...)). Behavior is correct, but you may simplify flow for clarity. No functional change required.
|
|
||
| function getPrimaryEmail(profile) { |
There was a problem hiding this comment.
In auth.routes.js POST /register and /login are not protected by guestOnlyMiddleware. The task requires registration and login to be available only to non‑authenticated users. Add guestOnlyMiddleware to these routes (e.g., authRouter.post('/register', guestOnlyMiddleware, authController.register)).
| 'use strict'; | ||
|
|
||
| const { sequelize } = require('../db'); | ||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const jwtService = require('./jwt.service'); | ||
| const userService = require('./user.service'); | ||
| const { GITHUB_PROVIDER } = require('./socialAccount.service'); | ||
|
|
||
| function getPrimaryEmail(profile) { | ||
| const email = profile.emails.find(Boolean) || ''; | ||
|
|
||
| return String(email).trim(); | ||
| } | ||
|
|
||
| function getDisplayName(profile, email) { | ||
| const fallbackName = email.split('@')[0] || 'GitHub user'; | ||
|
|
||
| return String(profile.displayName || profile.username || fallbackName).trim(); | ||
| } | ||
|
|
||
| async function finalizeGitHubAuthentication(profile, linkUserId = null) { | ||
| const providerUserId = String(profile.providerUserId || '').trim(); | ||
|
|
||
| if (!providerUserId) { | ||
| throw ApiError.badRequest('GitHub did not return a valid user id.'); | ||
| } | ||
|
|
||
| const email = getPrimaryEmail(profile); | ||
|
|
||
| if (!email) { | ||
| throw ApiError.badRequest('Error'); | ||
| } | ||
|
|
||
| const name = getDisplayName(profile, email); | ||
|
|
||
| return sequelize.transaction(async (transaction) => { | ||
| const existingSocialAccount = await SocialAccount.findOne({ | ||
| where: { | ||
| provider: GITHUB_PROVIDER, | ||
| providerUserId, | ||
| }, | ||
| transaction, | ||
| }); |
There was a problem hiding this comment.
There is no server-side logout route. The task requires logout be available only for authenticated users and redirect to login after logging out. Add a logout endpoint (protected by authMiddleware) that destroys session (if used) and/or advises frontend to clear token, then returns 204 or a JSON response. Frontend should call it and redirect to /login.
| 'use strict'; | ||
|
|
||
| const { sequelize } = require('../db'); | ||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const jwtService = require('./jwt.service'); | ||
| const userService = require('./user.service'); | ||
| const { GITHUB_PROVIDER } = require('./socialAccount.service'); | ||
|
|
||
| function getPrimaryEmail(profile) { | ||
| const email = profile.emails.find(Boolean) || ''; | ||
|
|
||
| return String(email).trim(); | ||
| } | ||
|
|
||
| function getDisplayName(profile, email) { | ||
| const fallbackName = email.split('@')[0] || 'GitHub user'; | ||
|
|
||
| return String(profile.displayName || profile.username || fallbackName).trim(); | ||
| } |
There was a problem hiding this comment.
guestOnlyMiddleware and authMiddleware inspect only the Authorization header (Bearer token). This matches the frontend which sets the header from localStorage, but the GitHub OAuth flows rely on express-session cookies. Ensure CORS and session cookie settings allow the session cookie across the OAuth redirect (sameSite, secure and credentials settings). This is operational guidance to avoid losing OAuth flow state.
| }; | ||
| } | ||
|
|
||
| let user = await userService.findByEmailCaseInsensitive(email, { | ||
| transaction, | ||
| }); | ||
|
|
||
| if (!user) { | ||
| user = await User.create( | ||
| { | ||
| name, | ||
| email, |
There was a problem hiding this comment.
saveSession in auth.controller.js calls req.session.save without checking if req.session exists. Given express-session is configured this should be fine, but add a guard if (!req.session) return Promise.resolve(); for extra robustness.
| if (!providerUserId) { | ||
| throw ApiError.badRequest('GitHub did not return a valid user id.'); | ||
| } | ||
|
|
||
| const email = getPrimaryEmail(profile); | ||
|
|
||
| if (!email) { | ||
| throw ApiError.badRequest('Error'); | ||
| } | ||
|
|
||
| const name = getDisplayName(profile, email); | ||
|
|
||
| return sequelize.transaction(async (transaction) => { | ||
| const existingSocialAccount = await SocialAccount.findOne({ | ||
| where: { | ||
| provider: GITHUB_PROVIDER, | ||
| providerUserId, | ||
| }, | ||
| transaction, | ||
| }); | ||
|
|
||
| if (linkUserId) { | ||
| const linkedUser = await userService.findById(linkUserId, { | ||
| transaction, | ||
| }); | ||
|
|
||
| if (!linkedUser) { | ||
| throw ApiError.notFound('User not found'); | ||
| } | ||
|
|
||
| if (existingSocialAccount) { | ||
| if (existingSocialAccount.userId !== linkedUser.id) { | ||
| throw ApiError.badRequest( | ||
| 'This GitHub account is already linked to another user.', | ||
| ); | ||
| } | ||
|
|
||
| return { | ||
| accessToken: null, | ||
| message: 'GitHub is already connected to your account.', | ||
| redirectPath: '/account', | ||
| }; | ||
| } | ||
|
|
||
| await SocialAccount.create( | ||
| { | ||
| provider: GITHUB_PROVIDER, | ||
| providerUserId, | ||
| providerEmail: email, | ||
| userId: linkedUser.id, | ||
| }, | ||
| { transaction }, | ||
| ); | ||
|
|
||
| return { | ||
| accessToken: null, | ||
| message: 'GitHub account connected successfully.', | ||
| redirectPath: '/account', | ||
| }; | ||
| } | ||
|
|
||
| if (existingSocialAccount) { | ||
| const existingUser = await userService.findById( | ||
| existingSocialAccount.userId, | ||
| { | ||
| transaction, | ||
| }, |
There was a problem hiding this comment.
Activation and email-change redirects include the access token in the URL query string. This works with the frontend ActivationPage which reads and stores the token, but exposing tokens in query params can leak them via referer or browser history. You already issue short-lived tokens; consider documenting this behavior or switching to a fragment/hash or POST if stricter security is desired.
| const { | ||
| validatePassword, |
There was a problem hiding this comment.
auth.routes.js: POST /register and POST /login are not using guestOnlyMiddleware. The task requires registration and login to be available only for non‑authenticated users — add guestOnlyMiddleware to these routes (e.g., authRouter.post('/register', guestOnlyMiddleware, authController.register)).
| /* eslint-disable indent */ | ||
| 'use strict'; | ||
|
|
||
| const bcrypt = require('bcryptjs'); | ||
| const { col, fn, where } = require('sequelize'); | ||
|
|
||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const { | ||
| validatePassword, | ||
| validateName, | ||
| validateEmail, | ||
| } = require('../utils/validate'); | ||
| const jwtService = require('./jwt.service'); | ||
| const mailService = require('./mail.service'); | ||
|
|
||
| function normalize(user) { | ||
| return { | ||
| id: user.id, | ||
| name: user.name, | ||
| email: user.email, | ||
| hasPassword: Boolean(user.password), | ||
| socialAccounts: Array.isArray(user.socialAccounts) | ||
| ? user.socialAccounts.map((socialAccount) => ({ | ||
| provider: socialAccount.provider, | ||
| })) | ||
| : [], | ||
| }; | ||
| } | ||
|
|
||
| function findByEmail(email, options = {}) { | ||
| return User.findOne({ where: { email }, ...options }); | ||
| } | ||
|
|
||
| function findByEmailCaseInsensitive(email, options = {}) { | ||
| return User.findOne({ | ||
| where: where(fn('lower', col('email')), String(email).trim().toLowerCase()), | ||
| ...options, | ||
| }); | ||
| } | ||
|
|
||
| function findByActivationToken(activationToken, options = {}) { | ||
| return User.findOne({ where: { activationToken }, ...options }); | ||
| } |
There was a problem hiding this comment.
No logout route exists in auth.routes.js or controllers. The task requires logout only for authenticated users and redirect to login after logging out. Add a protected logout endpoint (use authMiddleware) that destroys server session (if used) and returns success so frontend can clear token and navigate to /login.
| const nameError = validateName(newName); | ||
|
|
||
| if (nameError) { | ||
| throw ApiError.badRequest('Validation failed', { newName: nameError }); | ||
| } | ||
|
|
||
| const user = await findById(userId); | ||
|
|
||
| if (!user) { | ||
| throw ApiError.notFound('User not found'); | ||
| } | ||
|
|
There was a problem hiding this comment.
saveSession in auth.controller.js calls req.session.save without guarding for missing req.session. Express-session is configured, but for robustness consider adding if (!req.session) return Promise.resolve(); at start of saveSession to avoid potential runtime errors.
| throw ApiError.badRequest( | ||
| 'Set a password before changing your email address.', | ||
| { | ||
| password: 'Set a password before changing your email address.', | ||
| }, | ||
| ); | ||
| } | ||
|
|
||
| const errors = {}; | ||
| const normalizedEmail = String(newEmail).trim(); | ||
|
|
||
| if (!password) { | ||
| errors.password = 'Password is required'; | ||
| } | ||
|
|
||
| const emailError = validateEmail(normalizedEmail); | ||
|
|
There was a problem hiding this comment.
GitHub OAuth flows store intent in req.session.githubAuth. Ensure express-session cookie settings and CORS (credentials: true) permit the session cookie to be sent across OAuth redirects; otherwise session state may be lost on callback and flow will fallback. Verify sameSite/secure in production.
|
|
||
| function normalize(user) { | ||
| return { | ||
| id: user.id, | ||
| name: user.name, | ||
| email: user.email, | ||
| hasPassword: Boolean(user.password), |
There was a problem hiding this comment.
jwt.service.js: signing functions use environment variables directly (e.g., process.env.JWT_ACCESS_SECRET). If these are undefined, jwt.sign will throw. Ensure required JWT secrets are set in env for production. Consider validating presence at startup or providing clearer errors.
| validatePassword, | ||
| validateName, | ||
| validateEmail, | ||
| } = require('../utils/validate'); | ||
| const jwtService = require('./jwt.service'); | ||
| const mailService = require('./mail.service'); | ||
|
|
||
| function normalize(user) { | ||
| return { |
There was a problem hiding this comment.
mail.service.js: getTransport throws if GMAIL_USER / GMAIL_APP_PASSWORD missing. For development/testing consider a noop/mock transport to avoid runtime failures when sending emails is not configured.
|
|
||
| const { User } = require('../models/user.model'); | ||
| const { SocialAccount } = require('../models/socialAccount.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const { | ||
| validatePassword, | ||
| validateName, | ||
| validateEmail, | ||
| } = require('../utils/validate'); | ||
| const jwtService = require('./jwt.service'); | ||
| const mailService = require('./mail.service'); | ||
|
|
||
| function normalize(user) { | ||
| return { | ||
| id: user.id, | ||
| name: user.name, | ||
| email: user.email, | ||
| hasPassword: Boolean(user.password), | ||
| socialAccounts: Array.isArray(user.socialAccounts) | ||
| ? user.socialAccounts.map((socialAccount) => ({ | ||
| provider: socialAccount.provider, | ||
| })) | ||
| : [], | ||
| }; | ||
| } | ||
|
|
There was a problem hiding this comment.
guestOnlyMiddleware determines guest status solely from Authorization: Bearer header. This is consistent with frontend using bearer tokens. If you later support cookie/session auth for API calls, update this middleware to detect that too. For current app behavior no change required.
| function findByEmailCaseInsensitive(email, options = {}) { | ||
| return User.findOne({ | ||
| where: where(fn('lower', col('email')), String(email).trim().toLowerCase()), | ||
| ...options, | ||
| }); | ||
| } | ||
|
|
||
| function findByActivationToken(activationToken, options = {}) { | ||
| return User.findOne({ where: { activationToken }, ...options }); | ||
| } | ||
|
|
There was a problem hiding this comment.
socialAccount.model.js: indexes reference physical column names via field mappings (e.g., providerId, user_id). Ensure DB migrations/schema use the same column names so uniqueness indexes are applied correctly. This is informational — model is consistent as written.
| ## React Compiler | ||
|
|
There was a problem hiding this comment.
Add guestOnlyMiddleware to POST /register so only non-authenticated users can register. For example: authRouter.post('/register', guestOnlyMiddleware, authController.register); This is required by the task.
| ## React Compiler | ||
|
|
There was a problem hiding this comment.
Add guestOnlyMiddleware to POST /login so only non-authenticated users can log in. For example: authRouter.post('/login', guestOnlyMiddleware, authController.login); This is required by the task.
| # React + TypeScript + Vite | ||
|
|
||
| This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. | ||
|
|
||
| Currently, two official plugins are available: | ||
|
|
||
| - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) | ||
| - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) | ||
|
|
||
| ## React Compiler | ||
|
|
||
| The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). | ||
|
|
||
| ## Expanding the ESLint configuration | ||
|
|
||
| If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [ | ||
| // Other configs... | ||
|
|
||
| // Remove tseslint.configs.recommended and replace with this | ||
| tseslint.configs.recommendedTypeChecked, | ||
| // Alternatively, use this for stricter rules | ||
| tseslint.configs.strictTypeChecked, | ||
| // Optionally, add this for stylistic rules | ||
| tseslint.configs.stylisticTypeChecked, | ||
|
|
||
| // Other configs... | ||
| ], | ||
| languageOptions: { | ||
| parserOptions: { | ||
| project: ['./tsconfig.node.json', './tsconfig.app.json'], | ||
| tsconfigRootDir: import.meta.dirname, | ||
| }, | ||
| // other options... | ||
| }, | ||
| }, | ||
| ]) | ||
| ``` | ||
|
|
There was a problem hiding this comment.
There is no server-side logout route. The task requires logout to be available only for authenticated users and to redirect to login after logging out. Add a protected logout endpoint (use authMiddleware) that destroys server session (if used) and returns success so frontend can clear token and redirect to /login.
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [ |
There was a problem hiding this comment.
jwt.service.js relies on environment secrets such as JWT_ACCESS_SECRET. If these env vars are missing jwt.sign will throw. Validate required JWT secrets at startup or provide clear errors to avoid runtime failures in token operations (activation, reset, email-change).
|
|
||
| The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). | ||
|
|
||
| ## Expanding the ESLint configuration | ||
|
|
||
| If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), |
There was a problem hiding this comment.
mail.service.js throws when mail credentials are missing (GMAIL_USER / GMAIL_APP_PASSWORD). During development/tests this will break flows that send emails. Consider adding a development/noop transport or using an Ethereal account for local testing to avoid runtime exceptions.
|
|
||
| - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) | ||
| - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) | ||
|
|
||
| ## React Compiler | ||
|
|
||
| The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). | ||
|
|
||
| ## Expanding the ESLint configuration | ||
|
|
||
| If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: | ||
|
|
||
| ```js | ||
| export default defineConfig([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [ | ||
| // Other configs... | ||
|
|
||
| // Remove tseslint.configs.recommended and replace with this | ||
| tseslint.configs.recommendedTypeChecked, | ||
| // Alternatively, use this for stricter rules | ||
| tseslint.configs.strictTypeChecked, | ||
| // Optionally, add this for stylistic rules | ||
| tseslint.configs.stylisticTypeChecked, |
There was a problem hiding this comment.
guestOnlyMiddleware and authMiddleware inspect Authorization Bearer header (JWT). This matches frontend api.ts that sets the header. If you later support cookie/session auth for API calls, update these middlewares accordingly. No change required now.
| { | ||
| files: ['**/*.{ts,tsx}'], |
There was a problem hiding this comment.
In auth.routes.js, POST /register is not protected by guestOnlyMiddleware. The task requires registration to be available only to non-authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/register', guestOnlyMiddleware, authController.register);
| { | ||
| files: ['**/*.{ts,tsx}'], |
There was a problem hiding this comment.
In auth.routes.js, POST /login is not protected by guestOnlyMiddleware. The task requires login to be available only to non-authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/login', guestOnlyMiddleware, authController.login);
| ecmaFeatures: { | ||
| jsx: true, | ||
| }, | ||
| }, | ||
| }, | ||
| plugins: { | ||
| 'react-hooks': reactHooks, |
There was a problem hiding this comment.
jwt.service.js relies on environment secrets (e.g., JWT_ACCESS_SECRET). If these are missing, jwt.sign will throw at runtime. Consider validating required JWT env vars at startup and providing clear error messages to avoid runtime failures across flows that depend on JWTs (activation, reset, email-change).
| files: ['**/*.{ts,tsx}'], | ||
| extends: [js.configs.recommended, ...tseslint.configs.recommended], | ||
| languageOptions: { | ||
| ecmaVersion: 2020, | ||
| globals: globals.browser, | ||
| parserOptions: { | ||
| ecmaFeatures: { | ||
| jsx: true, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
mail.service.js: getTransport throws when MAIL_FROM_ADDRESS/GMAIL_USER or GMAIL_APP_PASSWORD are missing. During development or tests this will break email-dependent flows (registration, password reset). Consider adding a development/noop transport (or Ethereal) to allow testing without real SMTP credentials.
| import { globalIgnores } from 'eslint/config'; | ||
|
|
||
| export default tseslint.config([ | ||
| globalIgnores(['dist']), | ||
| { | ||
| files: ['**/*.{ts,tsx}'], | ||
| extends: [js.configs.recommended, ...tseslint.configs.recommended], | ||
| languageOptions: { | ||
| ecmaVersion: 2020, | ||
| globals: globals.browser, | ||
| parserOptions: { | ||
| ecmaFeatures: { | ||
| jsx: true, | ||
| }, | ||
| }, | ||
| }, | ||
| plugins: { | ||
| 'react-hooks': reactHooks, | ||
| 'react-refresh': reactRefresh, | ||
| }, | ||
| rules: { | ||
| ...reactHooks.configs.recommended.rules, | ||
| 'react-refresh/only-export-components': [ | ||
| 'warn', | ||
| { allowConstantExport: true }, | ||
| ], |
There was a problem hiding this comment.
guestOnlyMiddleware and authMiddleware inspect only the Authorization header for a Bearer token, which matches the frontend api.ts behavior. If you later support cookie/session-based API auth, update these middlewares accordingly. (No change required now.)
| import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage'; | ||
| import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage'; |
There was a problem hiding this comment.
auth.routes.js: POST /register is not protected by guestOnlyMiddleware. The task requires registration to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/register', guestOnlyMiddleware, authController.register);
| import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage'; | ||
| import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage'; |
There was a problem hiding this comment.
auth.routes.js: POST /login is not protected by guestOnlyMiddleware. The task requires login to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/login', guestOnlyMiddleware, authController.login);
| import { useEffect } from 'react'; | ||
| import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom'; | ||
| import { LoginPage } from './pages/LoginPage'; | ||
| import { RegisterPage } from './pages/RegisterPage'; | ||
| import { ActivationPage } from './pages/ActivationPage'; | ||
| import { AccountPage } from './pages/AccountPage'; | ||
| import { NotFoundPage } from './pages/NotFoundPage'; | ||
| import { ResetPasswordRequestPage } from './pages/ResetPasswordRequestPage'; | ||
| import { ResetPasswordEmailSentPage } from './pages/ResetPasswordEmailSentPage'; | ||
| import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage'; | ||
| import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage'; | ||
| import './styles.css'; | ||
|
|
||
| const pageTitles: Record<string, string> = { | ||
| '/': 'Log In', | ||
| '/login': 'Log In', | ||
| '/register': 'Sign Up', | ||
| '/reset-password': 'Reset Password', | ||
| '/reset-password/sent': 'Check Your Email', | ||
| '/reset-password/confirm': 'Choose New Password', | ||
| '/reset-password/success': 'Password Reset Complete', | ||
| '/activation': 'Email Verified', | ||
| '/account': 'My Account', | ||
| }; | ||
|
|
||
| function PageTitle() { | ||
| const { pathname } = useLocation(); | ||
|
|
||
| useEffect(() => { | ||
| document.title = pageTitles[pathname] ?? 'Page Not Found'; | ||
| }, [pathname]); | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| function App() { | ||
| return ( | ||
| <BrowserRouter> | ||
| <PageTitle /> | ||
| <Routes> | ||
| <Route path="/" element={<Navigate to="/login" replace />} /> | ||
| <Route path="/login" element={<LoginPage />} /> | ||
| <Route path="/register" element={<RegisterPage />} /> | ||
| <Route path="/reset-password" element={<ResetPasswordRequestPage />} /> | ||
| <Route |
There was a problem hiding this comment.
There is no server-side logout route in the backend. The task requires logout to be available only for authenticated users and to redirect to login after logging out. Add a protected logout endpoint (use authMiddleware) that destroys the server session (if any) and returns success so the frontend can clear token and navigate to /login.
| '/register': 'Sign Up', | ||
| '/reset-password': 'Reset Password', | ||
| '/reset-password/sent': 'Check Your Email', | ||
| '/reset-password/confirm': 'Choose New Password', | ||
| '/reset-password/success': 'Password Reset Complete', | ||
| '/activation': 'Email Verified', | ||
| '/account': 'My Account', |
There was a problem hiding this comment.
jwt.service.js relies on environment secrets (e.g., JWT_ACCESS_SECRET). If these are missing, jwt.sign will throw. Consider validating required JWT env vars at startup and providing clear errors to avoid runtime failures in flows that depend on JWTs (activation, password-reset, email-change).
| import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage'; | ||
| import './styles.css'; | ||
|
|
||
| const pageTitles: Record<string, string> = { | ||
| '/': 'Log In', | ||
| '/login': 'Log In', | ||
| '/register': 'Sign Up', | ||
| '/reset-password': 'Reset Password', | ||
| '/reset-password/sent': 'Check Your Email', | ||
| '/reset-password/confirm': 'Choose New Password', |
There was a problem hiding this comment.
mail.service.js: getTransport throws when MAIL_FROM_ADDRESS/GMAIL_USER or GMAIL_APP_PASSWORD are missing. During development/tests this will break email-dependent flows (registration, password reset). Consider adding a development/noop transport (or Ethereal) to allow testing without real SMTP credentials.
|
|
||
| useEffect(() => { |
There was a problem hiding this comment.
In src/backend/src/routes/auth.routes.js the POST /register route is not protected by guestOnlyMiddleware. The task requires registration to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/register', guestOnlyMiddleware, authController.register);
|
|
||
| useEffect(() => { |
There was a problem hiding this comment.
In src/backend/src/routes/auth.routes.js the POST /login route is not protected by guestOnlyMiddleware. The task requires login to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/login', guestOnlyMiddleware, authController.login);
|
|
||
| if (token) { | ||
| setAccessToken(token); | ||
| } | ||
|
|
||
| const timer = setTimeout(() => { | ||
| navigate('/account', { replace: true }); |
There was a problem hiding this comment.
src/backend/src/services/jwt.service.js relies on environment JWT secrets (e.g., JWT_ACCESS_SECRET). If these env vars are missing jwt.sign will throw. Consider validating required JWT env vars at startup and providing clear errors to avoid runtime failures in flows that use tokens (activation, password-reset, email-change).
| useEffect(() => { | ||
| document.title = isEmailChange ? 'Email Changed' : 'Email Verified'; | ||
| }, [isEmailChange]); | ||
|
|
||
| useEffect(() => { | ||
| const token = searchParams.get('accessToken'); | ||
|
|
||
| if (token) { | ||
| setAccessToken(token); | ||
| } |
There was a problem hiding this comment.
src/backend/src/services/mail.service.js throws when MAIL_FROM_ADDRESS/GMAIL_USER or GMAIL_APP_PASSWORD are missing. During development/tests this will break registration/password-reset flows that send emails. Consider adding a development/noop transport (or Ethereal) or handling missing creds gracefully in non-production to allow testing without real SMTP credentials.
|
|
||
| return <RegisterForm />; |
There was a problem hiding this comment.
In src/backend/src/routes/auth.routes.js the POST /register route is not protected by guestOnlyMiddleware. The task requires registration to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/register', guestOnlyMiddleware, authController.register);
|
|
||
| return <RegisterForm />; |
There was a problem hiding this comment.
In src/backend/src/routes/auth.routes.js the POST /login route is not protected by guestOnlyMiddleware. The task requires login to be available only to non‑authenticated users. Add guestOnlyMiddleware to this route, e.g.: authRouter.post('/login', guestOnlyMiddleware, authController.login);
| @@ -0,0 +1,2 @@ | |||
| node_modules | |||
There was a problem hiding this comment.
Password reset endpoints (/password-reset and /password-reset/confirm) are correctly using guestOnlyMiddleware in auth.routes.js, but ensure the activation and register/login routes use the same middleware for consistency with requirements.
| function isGithubAuthEnabled() { | ||
| return Boolean( | ||
| process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET, | ||
| ); | ||
| } |
There was a problem hiding this comment.
In passport.js lines 6-9: The isGithubAuthEnabled function checks process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET but returns Boolean(...,). The trailing comma is harmless in JS but the overall check is fine. No action required.
|
|
||
| res.json({ message: 'Name changed successfully.' }); | ||
| } catch (error) { | ||
| next(error); |
There was a problem hiding this comment.
Register and login routes are unprotected. According to the task, these must be available only to non-authenticated users. In src/backend/src/routes/auth.routes.js add guestOnlyMiddleware to POST /register and POST /login so only guests can access them.
| } | ||
|
|
||
| async function removeGithubAccount(req, res, next) { | ||
| try { |
There was a problem hiding this comment.
The activation endpoint GET /activate/:hash should be available only to non-authenticated users as per the requirements. In src/backend/src/routes/auth.routes.js wrap the /activate/:hash route with guestOnlyMiddleware.
|
|
||
| await userService.changeName({ userId: req.user.id, newName }); | ||
|
|
||
| res.json({ message: 'Name changed successfully.' }); |
There was a problem hiding this comment.
The app initializes passport with passport.initialize() but does not call passport.session(). You store GitHub flow state in req.session and use passport.authenticate with session:false, so this may be fine. If you intend to use Passport session deserialization later, consider adding app.use(passport.session()) in src/backend/src/app.js.
| await userService.requestEmailChange({ | ||
| userId: req.user.id, | ||
| password, | ||
| newEmail, | ||
| }); | ||
|
|
||
| res.json({ | ||
| message: 'Confirmation link sent to your new email address.', | ||
| }); |
There was a problem hiding this comment.
UserController.changeEmail (src/backend/src/controllers/user.controller.js) calls userService.requestEmailChange. Ensure the implementation of userService.requestEmailChange validates the current password, creates an email-change token (using jwtService.signEmailChangeToken), sends confirmation to the new email (mailService.sendEmailChangeConfirmation) and notifies the old email (mailService.sendEmailChangeNotification). This is required by the checklist item for changing email.
| next(error); | ||
| } |
There was a problem hiding this comment.
There is no logout endpoint in the provided auth routes. The requirements state "The application must allow a user to log out (only authenticated) and redirect to login after logging out." Add an authenticated POST /logout route (use authMiddleware) that destroys the session (use the existing destroySession logic) and responds so the client can redirect to /login.
| unique: true, | ||
| fields: ['user_id', 'provider'], | ||
| }, | ||
| ], | ||
| }, | ||
| ); | ||
|
|
||
| User.hasMany(SocialAccount, { | ||
| foreignKey: 'userId', | ||
| as: 'socialAccounts', | ||
| }); | ||
|
|
||
| SocialAccount.belongsTo(User, { |
There was a problem hiding this comment.
The controller relies on userService.requestEmailChange to implement the email-change initiation (require current password, send confirmation to new email, notify old email). Ensure that user.service.requestEmailChange exists and performs these steps; otherwise this violates the requirement: "To change an email on the Profile page, the user should type the password, confirm the new email and notify the old email about the change."
|
|
||
| const SocialAccount = sequelize.define( | ||
| 'SocialAccount', | ||
| { | ||
| id: { |
There was a problem hiding this comment.
me, changePassword, changeName, changeEmail and removeGithubAccount all forward errors via next(error). Ensure authMiddleware protects these routes (it is applied in user.routes.js) so they are available only to authenticated users as required.
| const { | ||
| validateName, |
There was a problem hiding this comment.
auth.routes.js: POST /register is not protected by guestOnlyMiddleware. The task requires registration to be available only to non-authenticated users — add guestOnlyMiddleware to this route.
| const { User } = require('../models/user.model'); | ||
| const { ApiError } = require('../utils/ApiError'); | ||
| const { | ||
| validateName, |
There was a problem hiding this comment.
auth.routes.js: POST /login is not protected by guestOnlyMiddleware. The task requires login to be available only to non-authenticated users — add guestOnlyMiddleware to this route.
| } | ||
|
|
||
| if (emailError) { | ||
| errors.email = emailError; |
There was a problem hiding this comment.
auth.routes.js: GET /activate/:hash is currently public. The task requires the activation page to be available only to non-authenticated users — wrap this route with guestOnlyMiddleware.
|
|
||
| if (Object.keys(errors).length > 0) { | ||
| throw ApiError.badRequest('Validation failed', errors); | ||
| } | ||
|
|
||
| const existing = await userService.findByEmail(email); | ||
|
|
||
| if (existing) { | ||
| throw ApiError.badRequest('Validation failed', { | ||
| email: 'Email is already taken', | ||
| }); | ||
| } | ||
|
|
There was a problem hiding this comment.
user.controller.js: changeEmail delegates to userService.requestEmailChange. Ensure that userService.requestEmailChange validates the provided password, generates an email-change confirmation token, sends the confirmation to the new email, and notifies the old email (mailService.sendEmailChangeConfirmation and sendEmailChangeNotification). If missing, implement these steps to satisfy the checklist.
| } | ||
|
|
||
| if (emailError) { | ||
| errors.email = emailError; |
There was a problem hiding this comment.
app.js: passport.initialize() is used but passport.session() is not mounted. Since you use express-session to store githubAuth state and use passport.authenticate with session:false, this may be intentional. If you require Passport session deserialization later, add app.use(passport.session()). Verify intended behavior for GitHub link/auth flows.
| userRouter.patch('/me/password', authMiddleware, userController.changePassword); | ||
| userRouter.patch('/me/name', authMiddleware, userController.changeName); |
There was a problem hiding this comment.
Per the task, registration must be available only to non-authenticated users. In src/backend/src/routes/auth.routes.js the POST /register route is not protected by guestOnlyMiddleware — add guestOnlyMiddleware to this route.
|
|
||
| userRouter.get('/me', authMiddleware, userController.me); | ||
| userRouter.patch('/me/password', authMiddleware, userController.changePassword); | ||
| userRouter.patch('/me/name', authMiddleware, userController.changeName); |
There was a problem hiding this comment.
Per the task, login must be available only to non-authenticated users. In src/backend/src/routes/auth.routes.js the POST /login route is not protected by guestOnlyMiddleware — add guestOnlyMiddleware to this route.
| type: DataTypes.INTEGER, | ||
| autoIncrement: true, |
There was a problem hiding this comment.
POST /register must be available only to non-authenticated users per the task. In src/backend/src/routes/auth.routes.js the route at line 10 registers '/register' without guestOnlyMiddleware. Add guestOnlyMiddleware to this route.
| { | ||
| id: { | ||
| type: DataTypes.INTEGER, | ||
| autoIncrement: true, |
There was a problem hiding this comment.
POST /login must be available only to non-authenticated users per the task. In src/backend/src/routes/auth.routes.js the route at line 11 registers '/login' without guestOnlyMiddleware. Add guestOnlyMiddleware to this route.
|
|
||
| authRouter.post('/register', authController.register); | ||
| authRouter.post('/login', authController.login); |
There was a problem hiding this comment.
In auth.routes.js lines 9-11 the POST /register and POST /login routes are unguarded. The task requires these be available only to non-authenticated users — add guestOnlyMiddleware to both routes.
| guestOnlyMiddleware, | ||
| authController.resetPassword, | ||
| ); | ||
| authRouter.get('/activate/:hash', authController.activate); |
There was a problem hiding this comment.
In auth.routes.js line 38 the GET /activate/:hash route is public. The task requires the activation page to be available only to non-authenticated users — wrap this route with guestOnlyMiddleware.
| authRouter.get( | ||
| '/email-change/confirm/:token', | ||
| authController.confirmEmailChange, | ||
| ); | ||
|
|
||
| module.exports = { authRouter }; |
There was a problem hiding this comment.
There is no logout endpoint in the provided auth routes. The task requires a logout action available only to authenticated users and redirect to login after logging out. Add an authenticated route (e.g. POST /logout) that calls session destroy (use the destroySession helper or req.session.destroy) and returns success so the client can redirect.
| guestOnlyMiddleware, | ||
| authController.resetPassword, | ||
| ); | ||
| authRouter.get('/activate/:hash', authController.activate); |
There was a problem hiding this comment.
In app.js line 38 you call passport.initialize() but not passport.session(). You store GitHub flow state in req.session and use passport.authenticate with session:false, so this may be intentional. If you plan to use Passport session deserialization later, add app.use(passport.session()). Otherwise confirm current behavior is intentional for GitHub flows.
|
|
||
| static badRequest(message, errors) { | ||
| return new ApiError({ status: 400, message, errors }); |
There was a problem hiding this comment.
In src/backend/src/routes/auth.routes.js lines 9-11: POST /register and POST /login are registered without guestOnlyMiddleware. The task requires registration and login to be available only to non-authenticated users — add guestOnlyMiddleware to both routes.
| return transport; | ||
| } | ||
|
|
There was a problem hiding this comment.
In src/backend/src/routes/auth.routes.js lines 9-11: POST /register and POST /login are not using guestOnlyMiddleware. The task explicitly requires these to be available only to non-authenticated users — add guestOnlyMiddleware to both routes.
| function getSender() { | ||
| const address = process.env.MAIL_FROM_ADDRESS || process.env.GMAIL_USER; | ||
|
|
||
| if (!address) { |
There was a problem hiding this comment.
In src/backend/src/routes/auth.routes.js line 38: GET /activate/:hash is currently public. The activation page must be available only to non-authenticated users per the requirements — wrap this route with guestOnlyMiddleware.
| 'use strict'; | ||
|
|
||
| const Nodemailer = require('nodemailer'); | ||
|
|
||
| let transport; | ||
|
|
||
| function getTransport() { | ||
| if (transport) { | ||
| return transport; | ||
| } | ||
|
|
||
| const user = process.env.GMAIL_USER || process.env.MAIL_FROM_ADDRESS; | ||
| const pass = process.env.GMAIL_APP_PASSWORD; | ||
|
|
||
| if (!user || !pass) { | ||
| throw new Error( | ||
| 'GMAIL_USER (or MAIL_FROM_ADDRESS) and GMAIL_APP_PASSWORD' + | ||
| ' are required to send emails through Gmail SMTP', | ||
| ); | ||
| } | ||
|
|
||
| transport = Nodemailer.createTransport({ | ||
| host: 'smtp.gmail.com', | ||
| port: 465, | ||
| secure: true, | ||
| auth: { | ||
| user, | ||
| pass, | ||
| }, | ||
| }); | ||
|
|
||
| return transport; | ||
| } | ||
|
|
||
| function getSender() { | ||
| const address = process.env.MAIL_FROM_ADDRESS || process.env.GMAIL_USER; | ||
|
|
||
| if (!address) { | ||
| throw new Error( | ||
| 'MAIL_FROM_ADDRESS or GMAIL_USER is required for the sender address', | ||
| ); | ||
| } | ||
|
|
||
| return { | ||
| address, |
There was a problem hiding this comment.
There is no logout route in the provided auth routes. The task requires a logout action available only to authenticated users and redirect to login after logging out. Add an authenticated route (e.g. POST /logout) protected by authMiddleware that destroys the server session (use req.session.destroy or a destroySession helper) and returns success so the client can redirect to /login.
| return { | ||
| address, | ||
| name: process.env.MAIL_FROM_NAME || 'Auth App', | ||
| }; | ||
| } | ||
|
|
||
| function send({ to, subject, text, html }) { | ||
| return getTransport().sendMail({ | ||
| from: getSender(), | ||
| to, | ||
| subject, | ||
| text, | ||
| html, |
There was a problem hiding this comment.
In src/backend/src/controllers/user.controller.js lines 44-56: changeEmail delegates to userService.requestEmailChange. The checklist requires that changing email: requires typing the password, confirms the new email (confirmation link to new email), and notifies the old email. Ensure userService.requestEmailChange verifies the current password, creates/signs an email-change token (jwtService.signEmailChangeToken), sends confirmation to the new email (mailService.sendEmailChangeConfirmation), and sends notification to the old email (mailService.sendEmailChangeNotification). If absent or incomplete, implement it.
| function getSender() { | ||
| const address = process.env.MAIL_FROM_ADDRESS || process.env.GMAIL_USER; | ||
|
|
||
| if (!address) { |
There was a problem hiding this comment.
In src/backend/src/app.js line 38: passport.initialize() is used but passport.session() is not mounted. You store GitHub flow state in req.session and use passport.authenticate with session:false, so this may be intentional. If you expect Passport to manage persistent sessions later, add app.use(passport.session()). Otherwise confirm current behavior is deliberate.
| function verifyToken(token, secret) { | ||
| try { | ||
| return jwt.verify(token, secret); |
There was a problem hiding this comment.
In src/backend/src/routes/auth.routes.js lines 9-11: POST /register and POST /login are not protected by guestOnlyMiddleware. The task requires registration and login to be available only to non-authenticated users — add guestOnlyMiddleware to both routes.
| } | ||
|
|
||
| function verifyEmailChangeToken(token) { | ||
| return verifyToken( |
There was a problem hiding this comment.
In src/backend/src/routes/auth.routes.js line 38: GET /activate/:hash is currently public. The task requires the activation page to be available only to non-authenticated users — wrap this route with guestOnlyMiddleware.
| 'use strict'; | ||
|
|
||
| const jwt = require('jsonwebtoken'); | ||
|
|
||
| function signToken(payload, secret, expiresIn) { | ||
| return jwt.sign(payload, secret, { expiresIn }); | ||
| } | ||
|
|
||
| function verifyToken(token, secret) { | ||
| try { | ||
| return jwt.verify(token, secret); | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| function sign(payload) { | ||
| return signToken( | ||
| payload, | ||
| process.env.JWT_ACCESS_SECRET, | ||
| process.env.JWT_ACCESS_EXPIRES || '1d', | ||
| ); | ||
| } | ||
|
|
||
| function verify(token) { | ||
| return verifyToken(token, process.env.JWT_ACCESS_SECRET); | ||
| } | ||
|
|
||
| function signEmailChangeToken(payload) { | ||
| return signToken( | ||
| payload, | ||
| process.env.JWT_EMAIL_CHANGE_SECRET || process.env.JWT_ACCESS_SECRET, | ||
| process.env.JWT_EMAIL_CHANGE_EXPIRES || '1h', | ||
| ); | ||
| } | ||
|
|
||
| function verifyEmailChangeToken(token) { | ||
| return verifyToken( | ||
| token, | ||
| process.env.JWT_EMAIL_CHANGE_SECRET || process.env.JWT_ACCESS_SECRET, | ||
| ); | ||
| } | ||
|
|
||
| function signPasswordResetToken(payload) { | ||
| return signToken( |
There was a problem hiding this comment.
There is no logout endpoint in the provided routes. The task requires an authenticated logout action that redirects to the login page. Add a protected route (e.g. POST /logout) using authMiddleware that destroys the session (use destroySession or req.session.destroy) and returns success so the client can redirect to /login.
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Sorry, the pull request review was unsuccessful for the following reasons:
Warning
Too many changes in the pull request. Please make sure that you have added only the necessary changes to the pull request.
[CODE: TMC]
If the issue is within your control, please address it.
The next review attempt will automatically launch in about 1 hour.
If you believe this is an error or need assistance, please contact the support team.
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Sorry, the pull request review was unsuccessful for the following reasons:
Warning
Too many changes in the pull request. Please make sure that you have added only the necessary changes to the pull request.
[CODE: TMC]
If the issue is within your control, please address it.
The next review attempt will automatically launch in about 1 hour.
If you believe this is an error or need assistance, please contact the support team.
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Sorry, the pull request review was unsuccessful for the following reasons:
Warning
Too many changes in the pull request. Please make sure that you have added only the necessary changes to the pull request.
[CODE: TMC]
If the issue is within your control, please address it.
The next review attempt will automatically launch in about 1 hour.
If you believe this is an error or need assistance, please contact the support team.
You can test the app here: https://node-auth-app-three.vercel.app/
The frontend has been deployed to Vercel, and the backend is hosted on my own Linux server.
I also used my domain superrobot.digital and Nginx to ensure the backend worked properly
For the first time ever, I tried using https://neon.com/ as a database (since it was built into Vercel). I think it’s a handy tool, but I’m not sure about the pricing—I need to look into that in more detail.