Skip to content

Task#270

Open
Augustwise wants to merge 16 commits intomate-academy:masterfrom
Augustwise:develop
Open

Task#270
Augustwise wants to merge 16 commits intomate-academy:masterfrom
Augustwise:develop

Conversation

@Augustwise
Copy link
Copy Markdown

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.

Copy link
Copy Markdown

@mateacademy-ai-mentor mateacademy-ai-mentor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

I am 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

  1. Rate AI review example

  2. Re-request AI review example

  3. Add AI review comments example

Comment on lines +12 to +18
async function register(req, res, next) {
try {
const { name, email, password } = req.body;

await authService.register({ name, email, password });

res.status(201).json({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +26 to +38
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()}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +44 to +49
async function login(req, res, next) {
try {
const { email = '', password = '' } = req.body;
const result = await authService.login({ email, password });

res.json(result);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +55 to +75
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.' });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +81 to +92
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()}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +135 to +151
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`,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +180 to +191
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +193 to +250

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +31 to +38

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +12 to +20
next(error);
}
}

async function changePassword(req, res, next) {
try {
const { oldPassword = '', newPassword = '' } = req.body;

const message = await userService.changePassword({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +6 to +21
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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +5 to +12

const User = sequelize.define(
'User',
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The errorMiddleware correctly serializes ApiError instances. Consider including stack trace in non-production environments for easier debugging, but not required by task.

Comment on lines +14 to +17
name: {
type: DataTypes.STRING,
allowNull: false,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +6 to +10
if (error instanceof ApiError) {
res.status(error.status).json({
message: error.message,
errors: error.errors,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error middleware returns ApiError errors property even when it's empty. That's fine. No functional issues found here.

Comment on lines +1 to +18
'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,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +61
'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 };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1 to +37
'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',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +5 to +17

function guestOnlyMiddleware(req, res, next) {
const header = req.headers.authorization || '';

if (!header) {
next();

return;
}

const [type, token] = header.split(' ');

if (type !== 'Bearer' || !token) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +6 to +31
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'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +31
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'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
const {
validateName,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +45
'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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +27 to +37
const errors = {};

const nameError = validateName(name);
const emailError = validateEmail(email);
const passwordError = validatePassword(password);

if (nameError) {
errors.name = nameError;
}

if (emailError) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/backend/src/routes/auth.routes.js Outdated
Comment on lines +9 to +11

authRouter.post('/register', authController.register);
authRouter.post('/login', authController.login);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +45
'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 };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1 to +34
'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',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +12 to +25

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +10 to +11
userRouter.patch('/me/password', authMiddleware, userController.changePassword);
userRouter.patch('/me/name', authMiddleware, userController.changeName);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1 to +11
'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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11

function getPrimaryEmail(profile) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +45
'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,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1 to +21
'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();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +111 to +122
};
}

let user = await userService.findByEmailCaseInsensitive(email, {
transaction,
});

if (!user) {
user = await User.create(
{
name,
email,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +26 to +92
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,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1 to +45
'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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +111 to +122
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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +26 to +92
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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +17 to +23
where: { provider, providerUserId },
...options,
});
}

function findByUserAndProvider(userId, provider, options = {}) {
return SocialAccount.findOne({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +11 to +20
provider: account.provider,
};
}

function findByProviderAccount(provider, providerUserId, options = {}) {
return SocialAccount.findOne({
where: { provider, providerUserId },
...options,
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +50 to +60
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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
try {
return jwt.verify(token, secret);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +10 to +11
try {
return jwt.verify(token, secret);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +45
'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(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +12 to +25
} catch {
return null;
}
}

function sign(payload) {
return signToken(
payload,
process.env.JWT_ACCESS_SECRET,
process.env.JWT_ACCESS_EXPIRES || '1d',
);
}

function verify(token) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
const {
validatePassword,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +45
/* 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 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +111 to +122
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');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +135 to +151
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +17 to +23

function normalize(user) {
return {
id: user.id,
name: user.name,
email: user.email,
hasPassword: Boolean(user.password),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +11 to +19
validatePassword,
validateName,
validateEmail,
} = require('../utils/validate');
const jwtService = require('./jwt.service');
const mailService = require('./mail.service');

function normalize(user) {
return {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +31

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,
}))
: [],
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +36 to +46
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 });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
static badRequest(message, errors) {
return new ApiError({ status: 400, message, errors });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +10 to +11
static badRequest(message, errors) {
return new ApiError({ status: 400, message, errors });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/.gitignore
Comment on lines +10 to +11
node_modules
dist
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/.gitignore
Comment on lines +10 to +11
node_modules
dist
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/.gitignore
Comment on lines +17 to +23
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/.gitignore
Comment on lines +11 to +20
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +10 to +11
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +12 to +25

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +17 to +23

return null;
}

function validatePassword(password) {
if (!password || password.length < 12) {
return 'Password must be at least 12 characters';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +11 to +20
}

function validateEmail(email) {
if (!email || !EMAIL_REGEX.test(email)) {
return 'Invalid email address';
}

return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +10 to +11
## React Compiler

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +10 to +11
## React Compiler

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +1 to +45
# 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...
},
},
])
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +17 to +23

```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/README.md
Comment on lines +11 to +20

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']),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

@mateacademy-ai-mentor mateacademy-ai-mentor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

I am 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):

  1. Guest-only protection on auth routes: In src/backend/src/routes/auth.routes.js, the routes POST /register and POST /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 insert guestOnlyMiddleware before 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 with guestOnlyMiddleware as well.
  2. 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 /logout in auth.routes.js, protect it with authMiddleware, and in its controller destroy the server session (using your existing session helper or req.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

  1. Rate AI review example

  2. Re-request AI review example

  3. Add AI review comments example

Comment on lines +6 to +21
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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +5 to +17

function guestOnlyMiddleware(req, res, next) {
const header = req.headers.authorization || '';

if (!header) {
next();

return;
}

const [type, token] = header.split(' ');

if (type !== 'Bearer' || !token) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +6 to +31
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'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +31
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'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11

function getPrimaryEmail(profile) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +45
'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,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1 to +21
'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();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +111 to +122
};
}

let user = await userService.findByEmailCaseInsensitive(email, {
transaction,
});

if (!user) {
user = await User.create(
{
name,
email,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +26 to +92
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,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
const {
validatePassword,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +45
/* 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 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +111 to +122
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');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +135 to +151
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +17 to +23

function normalize(user) {
return {
id: user.id,
name: user.name,
email: user.email,
hasPassword: Boolean(user.password),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +11 to +19
validatePassword,
validateName,
validateEmail,
} = require('../utils/validate');
const jwtService = require('./jwt.service');
const mailService = require('./mail.service');

function normalize(user) {
return {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +31

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,
}))
: [],
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +36 to +46
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 });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +10 to +11
## React Compiler

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +10 to +11
## React Compiler

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +1 to +45
# 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...
},
},
])
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +17 to +23

```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/README.md
Comment on lines +11 to +20

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']),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +6 to +31

- [@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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
{
files: ['**/*.{ts,tsx}'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +10 to +11
{
files: ['**/*.{ts,tsx}'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +17 to +23
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
'react-hooks': reactHooks,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +11 to +20
files: ['**/*.{ts,tsx}'],
extends: [js.configs.recommended, ...tseslint.configs.recommended],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +31
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 },
],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/src/App.tsx
Comment on lines +10 to +11
import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage';
import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/src/App.tsx
Comment on lines +10 to +11
import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage';
import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

@mateacademy-ai-mentor mateacademy-ai-mentor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

I am 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):

  1. Guest-only protection on auth routes: In src/backend/src/routes/auth.routes.js, the routes POST /register and POST /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 insert guestOnlyMiddleware before 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 with guestOnlyMiddleware as well.
  2. 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 /logout in auth.routes.js, protect it with authMiddleware, and in its controller destroy the server session (using your existing session helper or req.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

  1. Rate AI review example

  2. Re-request AI review example

  3. Add AI review comments example

Comment on lines +6 to +21
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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +5 to +17

function guestOnlyMiddleware(req, res, next) {
const header = req.headers.authorization || '';

if (!header) {
next();

return;
}

const [type, token] = header.split(' ');

if (type !== 'Bearer' || !token) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +6 to +31
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'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +31
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'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11

function getPrimaryEmail(profile) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +45
'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,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1 to +21
'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();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +111 to +122
};
}

let user = await userService.findByEmailCaseInsensitive(email, {
transaction,
});

if (!user) {
user = await User.create(
{
name,
email,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +26 to +92
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,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
const {
validatePassword,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +45
/* 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 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +111 to +122
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');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +135 to +151
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +17 to +23

function normalize(user) {
return {
id: user.id,
name: user.name,
email: user.email,
hasPassword: Boolean(user.password),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +11 to +19
validatePassword,
validateName,
validateEmail,
} = require('../utils/validate');
const jwtService = require('./jwt.service');
const mailService = require('./mail.service');

function normalize(user) {
return {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +31

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,
}))
: [],
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +36 to +46
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 });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +10 to +11
## React Compiler

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +10 to +11
## React Compiler

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +1 to +45
# 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...
},
},
])
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +17 to +23

```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/README.md
Comment on lines +11 to +20

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']),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +6 to +31

- [@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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
{
files: ['**/*.{ts,tsx}'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +10 to +11
{
files: ['**/*.{ts,tsx}'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +17 to +23
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
'react-hooks': reactHooks,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +11 to +20
files: ['**/*.{ts,tsx}'],
extends: [js.configs.recommended, ...tseslint.configs.recommended],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +31
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 },
],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/src/App.tsx
Comment on lines +10 to +11
import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage';
import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/src/App.tsx
Comment on lines +10 to +11
import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage';
import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/src/App.tsx
Comment on lines +1 to +45
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/src/App.tsx
Comment on lines +17 to +23
'/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',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/src/App.tsx
Comment on lines +11 to +20
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',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11

useEffect(() => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +10 to +11

useEffect(() => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +17 to +23

if (token) {
setAccessToken(token);
}

const timer = setTimeout(() => {
navigate('/account', { replace: true });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +11 to +20
useEffect(() => {
document.title = isEmailChange ? 'Email Changed' : 'Email Verified';
}, [isEmailChange]);

useEffect(() => {
const token = searchParams.get('accessToken');

if (token) {
setAccessToken(token);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11

return <RegisterForm />;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +10 to +11

return <RegisterForm />;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/backend/.gitignore
@@ -0,0 +1,2 @@
node_modules
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +10
function isGithubAuthEnabled() {
return Boolean(
process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET,
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +48 to +56
await userService.requestEmailChange({
userId: req.user.id,
password,
newEmail,
});

res.json({
message: 'Confirmation link sent to your new email address.',
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +40 to +41
next(error);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +44 to +56
unique: true,
fields: ['user_id', 'provider'],
},
],
},
);

User.hasMany(SocialAccount, {
foreignKey: 'userId',
as: 'socialAccounts',
});

SocialAccount.belongsTo(User, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +6 to +10

const SocialAccount = sequelize.define(
'SocialAccount',
{
id: {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
const {
validateName,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +44 to +56

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
userRouter.patch('/me/password', authMiddleware, userController.changePassword);
userRouter.patch('/me/name', authMiddleware, userController.changeName);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
type: DataTypes.INTEGER,
autoIncrement: true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/backend/src/routes/auth.routes.js Outdated
Comment on lines +9 to +11

authRouter.post('/register', authController.register);
authRouter.post('/login', authController.login);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +40 to +45
authRouter.get(
'/email-change/confirm/:token',
authController.confirmEmailChange,
);

module.exports = { authRouter };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

@mateacademy-ai-mentor mateacademy-ai-mentor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Summary

I am 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):

  1. Guest-only protection on auth routes: In src/backend/src/routes/auth.routes.js, the routes POST /register and POST /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 insert guestOnlyMiddleware before 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 with guestOnlyMiddleware as well.
  2. 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 /logout in auth.routes.js, protect it with authMiddleware, and in its controller destroy the server session (using your existing session helper or req.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

  1. Rate AI review example

  2. Re-request AI review example

  3. Add AI review comments example

Comment on lines +6 to +21
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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +5 to +17

function guestOnlyMiddleware(req, res, next) {
const header = req.headers.authorization || '';

if (!header) {
next();

return;
}

const [type, token] = header.split(' ');

if (type !== 'Bearer' || !token) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Comment on lines +6 to +31
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'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +31
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'));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11

function getPrimaryEmail(profile) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +45
'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,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1 to +21
'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();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +111 to +122
};
}

let user = await userService.findByEmailCaseInsensitive(email, {
transaction,
});

if (!user) {
user = await User.create(
{
name,
email,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +26 to +92
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,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
const {
validatePassword,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +1 to +45
/* 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 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +111 to +122
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');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +135 to +151
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +17 to +23

function normalize(user) {
return {
id: user.id,
name: user.name,
email: user.email,
hasPassword: Boolean(user.password),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +11 to +19
validatePassword,
validateName,
validateEmail,
} = require('../utils/validate');
const jwtService = require('./jwt.service');
const mailService = require('./mail.service');

function normalize(user) {
return {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +31

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,
}))
: [],
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +36 to +46
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 });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +10 to +11
## React Compiler

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +10 to +11
## React Compiler

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +1 to +45
# 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...
},
},
])
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +17 to +23

```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/README.md
Comment on lines +11 to +20

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']),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/README.md
Comment on lines +6 to +31

- [@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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
{
files: ['**/*.{ts,tsx}'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +10 to +11
{
files: ['**/*.{ts,tsx}'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +17 to +23
ecmaFeatures: {
jsx: true,
},
},
},
plugins: {
'react-hooks': reactHooks,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +11 to +20
files: ['**/*.{ts,tsx}'],
extends: [js.configs.recommended, ...tseslint.configs.recommended],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +31
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 },
],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/src/App.tsx
Comment on lines +10 to +11
import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage';
import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/src/App.tsx
Comment on lines +10 to +11
import { ResetPasswordConfirmPage } from './pages/ResetPasswordConfirmPage';
import { ResetPasswordSuccessPage } from './pages/ResetPasswordSuccessPage';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/src/App.tsx
Comment on lines +1 to +45
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/frontend/src/App.tsx
Comment on lines +17 to +23
'/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',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/frontend/src/App.tsx
Comment on lines +11 to +20
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',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11

useEffect(() => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +10 to +11

useEffect(() => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +17 to +23

if (token) {
setAccessToken(token);
}

const timer = setTimeout(() => {
navigate('/account', { replace: true });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +11 to +20
useEffect(() => {
document.title = isEmailChange ? 'Email Changed' : 'Email Verified';
}, [isEmailChange]);

useEffect(() => {
const token = searchParams.get('accessToken');

if (token) {
setAccessToken(token);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11

return <RegisterForm />;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +10 to +11

return <RegisterForm />;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/backend/.gitignore
@@ -0,0 +1,2 @@
node_modules
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +6 to +10
function isGithubAuthEnabled() {
return Boolean(
process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET,
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +48 to +56
await userService.requestEmailChange({
userId: req.user.id,
password,
newEmail,
});

res.json({
message: 'Confirmation link sent to your new email address.',
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +40 to +41
next(error);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +44 to +56
unique: true,
fields: ['user_id', 'provider'],
},
],
},
);

User.hasMany(SocialAccount, {
foreignKey: 'userId',
as: 'socialAccounts',
});

SocialAccount.belongsTo(User, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +6 to +10

const SocialAccount = sequelize.define(
'SocialAccount',
{
id: {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
const {
validateName,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +44 to +56

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
userRouter.patch('/me/password', authMiddleware, userController.changePassword);
userRouter.patch('/me/name', authMiddleware, userController.changeName);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +10 to +11
type: DataTypes.INTEGER,
autoIncrement: true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/backend/src/routes/auth.routes.js Outdated
Comment on lines +9 to +11

authRouter.post('/register', authController.register);
authRouter.post('/login', authController.login);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +40 to +45
authRouter.get(
'/email-change/confirm/:token',
authController.confirmEmailChange,
);

module.exports = { authRouter };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +9 to +11

static badRequest(message, errors) {
return new ApiError({ status: 400, message, errors });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +9 to +11
return transport;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1 to +45
'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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +44 to +56
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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +9 to +11
function verifyToken(token, secret) {
try {
return jwt.verify(token, secret);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1 to +45
'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(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

@mateacademy-ai-mentor mateacademy-ai-mentor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

@mateacademy-ai-mentor mateacademy-ai-mentor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

@mateacademy-ai-mentor mateacademy-ai-mentor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

@brespect brespect left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm 🚀

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants