Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { User } from '../models/user.js';

import { userService } from '../services/user.services.js';
import { tokenService } from '../services/token.service.js';
import { jwtService } from '../services/jwt.service.js';
import { ApiError } from '../exeptions/api.error.js';
import bcrypt from 'bcrypt';

function validateEmail(value) {
const EMAIL_PATTERN = /^[\w.+-]+@([\w-]+\.){1,3}[\w-]{2,}$/;

if (!value) {
return 'Email is required';
}

if (!EMAIL_PATTERN.test(value)) {
return 'Email is not valid';
}
}

const validatePassword = (value) => {
if (!value) {
return 'Password is required';
}

if (value.length < 6) {
return 'At least 6 characters';
}
};
const validateName = (value) => {
if (!value) {
return 'Name is last';

Choose a reason for hiding this comment

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

This error message appears to have a typo. It should probably be something like 'Name is required'.

Choose a reason for hiding this comment

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

It looks like there's a small typo in this error message. It probably should be 'Name is required' to be consistent with the other validation messages.

}

if (value.length < 2) {
return 'At least 2 characters';
}
};

const register = async (req, res, next) => {
const { name, email, password } = req.body;
const errors = {
name: validateName(name),
email: validateEmail(email),
password: validatePassword(password),
};

if (errors.email || errors.password || errors.name) {
throw ApiError.badRequest('Bad request', errors);
}

const hashedPass = await bcrypt.hash(password, 10);

await userService.register(name, email, hashedPass);
res.send({ message: 'OK' });
};

const activate = async (req, res) => {
const { activationToken } = req.params;
const user = await User.findOne({ where: { activationToken } });

if (!user) {
res.sendStatus(404);

return;
}
user.activationToken = null;

await user.save();
res.send(user);

Choose a reason for hiding this comment

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

According to the requirements, you should redirect the user to the Profile page after successful activation. This implementation sends the user object as a JSON response instead.

Choose a reason for hiding this comment

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

Good job implementing the activation logic. However, there are two points to address here to fully meet the requirements:

  1. The requirement is to "redirect to Profile after the activation". Since the profile page requires authentication, this endpoint should log the user in by generating and returning access tokens. You can call the generateTokens function here.
  2. The response currently sends the entire user database object, which can expose sensitive data like the password hash. It's best practice to send a normalized user object, similar to what generateTokens does.

};

const login = async (req, res) => {
const { email, password } = req.body;
const user = await userService.findByEmail(email);

if (!user) {
throw ApiError.badRequest('No such user');
}

Choose a reason for hiding this comment

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

The requirements specify that if a user is not active, you should ask them to activate their email. A check is missing here to verify if the user's account has been activated (i.e., user.activationToken is null) before proceeding with the login.

if (user.activationToken !== null) {
throw ApiError.badRequest('you must activated your email check your box');
}

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

if (!isPasswordValid) {
throw ApiError.badRequest('Wrong password');
}
await generateTokens(res, user);
};
const refresh = async (req, res) => {
const { refreshToken } = req.cookies;

if (!refreshToken) {
throw ApiError.unauthorized();
}

const payload = await jwtService.verifyRefresh(refreshToken);

if (!payload) {
throw ApiError.unauthorized();
}

const tokenRecord = await tokenService.getByToken(refreshToken);

if (!tokenRecord) {
throw ApiError.unauthorized();
}

const { user: payloadUser } = payload;

const user = await userService.findByEmail(payloadUser.email);

if (!user) {
throw ApiError.unauthorized();
}

generateTokens(res, user);

Choose a reason for hiding this comment

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

The generateTokens function is async and performs database operations. It's important to await its result here to ensure any potential errors are caught and handled correctly by your catchError middleware. Without await, an error inside generateTokens could lead to an unhandled promise rejection.

};

const generateTokens = async (res, user) => {
const normalizeUser = userService.normalize(user);
const accessToken = jwtService.sign(normalizeUser);
const refreshAccessToken = jwtService.signRefresh(normalizeUser);

await tokenService.save(normalizeUser.id, refreshAccessToken);

res.cookie('refreshToken', refreshAccessToken, {
maxAge: 30 * 24 * 60 * 60 * 1000,
httpOnly: true,
});

res.send({
user: normalizeUser,
accessToken,
});
};

const logout = async (req, res) => {
const { refreshToken } = req.cookies;
const payload = await jwtService.verifyRefresh(refreshToken);

if (!refreshToken || !payload) {
throw ApiError.unauthorized();
}

await tokenService.remove(payload.user.id);
res.clearCookie('refreshToken', { httpOnly: true });
res.sendStatus(204);

Choose a reason for hiding this comment

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

The task requires redirecting the user to the login page after logging out. Sending a 204 No Content status is a common practice for APIs, but it doesn't fulfill the specific requirement for a redirect.

};

const resetPassword = async (req, res) => {
const { email } = req.body;
const errors = {
email: validateEmail(email),
};

if (errors.email) {
throw ApiError.badRequest('Bad request', errors);
}

await userService.resetPassword(email);
res.send({ message: 'email send' });
};

const confirm = async (req, res) => {
const { password, reppassword, token } = req.body;

await userService.confirmReset(password, reppassword, token);
res.sendStatus(204);
};

export const authController = {
register,
activate,
login,
refresh,
logout,
resetPassword,
confirm,
validateEmail,
};
154 changes: 154 additions & 0 deletions src/controllers/user.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { ApiError } from '../exeptions/api.error.js';
import { User } from '../models/user.js';
import { userService } from '../services/user.services.js';
import bcrypt from 'bcrypt';
import { authController } from './auth.controller.js';
import { emailServices } from '../services/mail.services.js';
import { v4 as uuidv4 } from 'uuid';

const getAllActivated = async (req, res) => {
const user = await userService.getAllActivated();

res.send(user.map(userService.normalize));
};

const changeName = async (req, res) => {
const { name } = req.body;
const userId = req.user.id;

if (name.length === 0) {
throw ApiError.badRequest('name is empty');
}

if (!userId) {
throw ApiError.badRequest('unautorized');
}
await User.update({ name }, { where: { id: userId } });

res.json({ message: 'Name updated successfully' });
};

const changePassword = async (req, res) => {
const { password, newPassword, confirmPassword } = req.body;

const userId = req.user.id;

if (!password || !newPassword || !confirmPassword) {
throw ApiError.badRequest('some passwords are empty');
}

if (newPassword !== confirmPassword) {
throw ApiError.badRequest('New password and confirmation do not match');
}

if (!userId) {
throw ApiError.badRequest('user unatorization');
}

const user = await User.findOne({ where: { id: userId } });

if (!user) {
throw ApiError.unauthorized('user unauthorized');
}

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

if (!isPasswordValid) {
throw ApiError.badRequest('Wrong password');
}

const hashedPass = await bcrypt.hash(newPassword, 10);

user.password = hashedPass;
await user.save();
res.json({ message: 'Password updated successfully' });
};

const changeEmail = async (req, res) => {
const { email, password } = req.body;
const userId = req.user.id;

if (!email || !password) {
throw ApiError.badRequest('empty email or password');
}

const errors = {
email: authController.validateEmail(email),
};

if (errors.email) {
throw ApiError.badRequest('error email');
}

const user = await User.findOne({ where: { id: userId } });

if (!user) {
throw ApiError.unauthorized('unauthorized');
}

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

if (!isPasswordValid) {
throw ApiError.badRequest('wrong password');
}

const token = uuidv4();

user.resetEmailToken = token;
user.newEmail = email;
await user.save();

await emailServices.sendChangeNewEmail(user.email, email, token);

res.send({ message: 'email sent' });
};

const confirmNewEmail = async (req, res) => {
const { token } = req.query;
const activeToken = uuidv4();

if (!token) {
throw ApiError.badRequest('invalid token');
}

const findUser = await User.findOne({ where: { resetEmailToken: token } });

if (!findUser) {
throw ApiError.badRequest(' user not found');
}

findUser.resetEmailToken = null;
findUser.activationToken = activeToken;
await findUser.save();
await emailServices.sendNewEmail(findUser.newEmail, activeToken);

res.send(200);
};

const finallConfirm = async (req, res) => {
const { token } = req.query;

if (!token) {
throw ApiError.badRequest('invalid token');
}

const findUser = await User.findOne({ where: { activationToken: token } });

if (!findUser) {
throw ApiError.badRequest('no user');
}
findUser.email = findUser.newEmail;
findUser.newEmail = null;
findUser.activationToken = null;
await findUser.save();
Comment on lines 140 to 143

Choose a reason for hiding this comment

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

The activationToken should be cleared after it has been used to prevent it from being used again. Reusing the token could lead to unexpected errors, such as setting the user's email to null if the link is clicked a second time. Consider setting findUser.activationToken = null; before saving the user.

res.json('email is active now ');
};

export const userController = {
getAllActivated,
changeName,
changePassword,
changeEmail,
confirmNewEmail,
finallConfirm,
};
35 changes: 35 additions & 0 deletions src/createServer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import express from 'express';
import 'dotenv/config';
import { authRouter } from './routes/auth.route.js';
import cors from 'cors';
import { userRouter } from './routes/user.route.js';
import { errorMiddlewares } from './middlewares/errorMiddlewares.js';
import cookieParser from 'cookie-parser';

export function createServer() {
const app = express();

app.use(express.json());
app.use(cookieParser());

app.use(
cors({
origin: process.env.CLIENT_HOST,
credentials: true,
}),
);

app.use(authRouter);
app.use('/users', userRouter);

app.get('/', (req, res) => {
res.send('hello');
});
app.use(errorMiddlewares);

Choose a reason for hiding this comment

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

According to the task requirements, you need to implement a 404 page for all other pages. This file is missing a catch-all middleware to handle requests to undefined routes. You should add a middleware before this error handler to catch any requests that haven't been matched by a route and respond with a 404 status.


app.use('*', (req, res) => {
res.status(404).json({ message: 'Not Found' });
});
Comment on lines +28 to +32

Choose a reason for hiding this comment

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

Great job adding the 404 handler! While this works, it's a common convention in Express to place the catch-all 404 handler before the general error middleware (errorMiddlewares). This creates a more logical flow where unmatched routes are handled first, and then any errors are caught by the final error handler.


return app;
}
Loading