-
Notifications
You must be signed in to change notification settings - Fork 303
Develop #206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Develop #206
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
|
||
| }; | ||
|
|
||
| const login = async (req, res) => { | ||
| const { email, password } = req.body; | ||
| const user = await userService.findByEmail(email); | ||
|
|
||
| if (!user) { | ||
| throw ApiError.badRequest('No such user'); | ||
| } | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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., |
||
| 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| }; | ||
|
|
||
| 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| }; | ||
|
|
||
| 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, | ||
| }; | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| res.json('email is active now '); | ||
| }; | ||
|
|
||
| export const userController = { | ||
| getAllActivated, | ||
| changeName, | ||
| changePassword, | ||
| changeEmail, | ||
| confirmNewEmail, | ||
| finallConfirm, | ||
| }; | ||
| 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ( |
||
|
|
||
| return app; | ||
| } | ||
There was a problem hiding this comment.
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'.