-
Notifications
You must be signed in to change notification settings - Fork 347
Solution #253
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?
Solution #253
Changes from 1 commit
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,16 @@ | ||
| /* eslint-disable no-console */ | ||
| require('dotenv').config(); | ||
|
|
||
| const { Sequelize } = require('sequelize'); | ||
|
|
||
| const sequelize = new Sequelize( | ||
| process.env.DATABASE, | ||
| process.env.USERNAME_DATABASE, | ||
| process.env.PASSWORD_DATABASE, | ||
| { | ||
| host: process.env.HOST_DATABASE, | ||
| dialect: 'postgres', | ||
| }, | ||
| ); | ||
|
|
||
| module.exports = { sequelize }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| /* eslint-disable no-console */ | ||
| const User = require('../models/user.model'); | ||
| const bcrypt = require('bcrypt'); | ||
| const { | ||
| validateEmail, | ||
| validatePassword, | ||
| send, | ||
| } = require('../services/auth.services'); | ||
| const { v4: uuidv4 } = require('uuid'); | ||
| const jwt = require('jsonwebtoken'); | ||
|
|
||
| const registration = async (req, res) => { | ||
| const { name, email, password } = req.body; | ||
| const errorEmail = validateEmail(email); | ||
| const errorPassword = validatePassword(password); | ||
|
|
||
| if (errorEmail) { | ||
| return res.status(400).send(errorEmail); | ||
| } | ||
|
|
||
| if (errorPassword) { | ||
| return res.status(400).send(errorPassword); | ||
| } | ||
|
|
||
| const exist = await User.findOne({ where: { email } }); | ||
|
|
||
| if (exist !== null) { | ||
| return res.status(401).send('User already exist'); | ||
| } | ||
|
|
||
| const cashedPassword = await bcrypt.hash(password, 10); | ||
|
|
||
| const uuid = uuidv4(); | ||
|
|
||
| await User.create({ | ||
| name: name, | ||
| email: email, | ||
| password: cashedPassword, | ||
| activationToken: uuid, | ||
| }); | ||
|
|
||
| await send( | ||
| email, | ||
| 'Activate email', | ||
| `go to link http://localhost:3000/activation/${uuid}`, | ||
| ); | ||
| res.send(201); | ||
| }; | ||
|
|
||
| const activation = async (req, res) => { | ||
| const { activationToken } = req.params; | ||
|
|
||
| const user = await User.findOne({ where: { activationToken } }); | ||
|
|
||
| if (!user) { | ||
| res.status(404).send('invalind activation token'); | ||
|
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. When the activation token is invalid, you send a 404 response but don't stop the function's execution. The code will then proceed to line 59 and try to access properties of |
||
| } | ||
|
|
||
| user.isActivated = true; | ||
| user.activationToken = null; | ||
| await user.save(); | ||
| res.redirect('http://localhost:3000/profile'); | ||
|
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 to the profile page after activation, which you've done. However, the profile page is protected and requires the user to be authenticated. Currently, after activation, the user isn't logged in, so they'll be blocked from accessing the profile. To fix this, you should log the user in by creating a JWT and setting it as a cookie, just like you do in the |
||
| }; | ||
|
|
||
| const login = async (req, res) => { | ||
| const { email, password } = req.body; | ||
|
|
||
| const existUser = await User.findOne({ where: { email } }); | ||
|
|
||
| if (!existUser) { | ||
| res.send('User not found'); | ||
|
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. If the user does not exist, you send a response but do not stop the function's execution. The code will then attempt to access |
||
| } | ||
|
|
||
| const isValid = await bcrypt.compare(password, existUser.password); | ||
|
|
||
| if (!isValid) { | ||
| res.send('Invalid credentials'); | ||
|
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. Similarly, if the password is not valid, you should |
||
| } | ||
|
|
||
| if (!existUser.isActivated) { | ||
| await send( | ||
| email, | ||
| 'Activate email', | ||
| `go to link http://localhost:3000/activation/${existUser.activationToken}`, | ||
| ); | ||
|
|
||
| return res.status(403).send('Activate email first'); | ||
| } | ||
|
|
||
| const token = jwt.sign( | ||
| { id: existUser.id, email: existUser.email }, | ||
| process.env.JWT_SECRET, | ||
| { expiresIn: '30 days' }, | ||
| ); | ||
|
|
||
| res.cookie('token', token, { httpOnly: true }); | ||
| res.redirect('http://localhost:3000/profile'); | ||
| }; | ||
|
|
||
| module.exports = { | ||
| authController: { | ||
| registration, | ||
| activation, | ||
| login, | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| /* eslint-disable no-console */ | ||
| const User = require('../models/user.model'); | ||
| const bcrypt = require('bcrypt'); | ||
| const { v4: uuidv4 } = require('uuid'); | ||
|
|
||
| const { | ||
| validateEmail, | ||
| validatePassword, | ||
| send, | ||
| } = require('../services/auth.services'); | ||
|
|
||
| const profile = async (req, res) => { | ||
| const userId = req.user.id; | ||
|
|
||
| const user = await User.findByPk(userId, { | ||
| attributes: ['id', 'name', 'email'], | ||
| }); | ||
|
|
||
| res.send(user); | ||
| }; | ||
|
|
||
| const changeName = async (req, res) => { | ||
| const userId = req.user.id; | ||
| const { name } = req.body; | ||
|
|
||
| const user = await User.findByPk(userId); | ||
|
|
||
| user.name = name; | ||
| await user.save(); | ||
| res.send('Username changed'); | ||
| }; | ||
|
|
||
| const changePassword = async (req, res) => { | ||
| const userId = req.user.id; | ||
| const { password, newPassword, confirm } = req.body; | ||
|
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, this function should expect fields named |
||
|
|
||
| if (!confirm) { | ||
|
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 a |
||
| res.send('You need confirm change'); | ||
| } | ||
|
|
||
| const user = await User.findByPk(userId); | ||
|
|
||
| const isValid = await bcrypt.compare(password, user.password); | ||
|
|
||
| if (!isValid) { | ||
| res.status(401).send('Invalid credentials'); | ||
|
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. After sending a response for an invalid old password, you must add a |
||
| } | ||
|
|
||
| user.password = await bcrypt.hash(newPassword, 10); | ||
| await user.save(); | ||
| res.send('password changed'); | ||
| }; | ||
|
|
||
| const resetPassword = async (req, res) => { | ||
| const { email } = req.body; | ||
|
|
||
| const user = await User.findOne({ where: { email } }); | ||
|
|
||
| if (!user) { | ||
| return res.status(401).send('Invalid credentials'); | ||
| } | ||
|
|
||
| const token = uuidv4(); | ||
|
|
||
| user.resetToken = token; | ||
|
|
||
| await send( | ||
| user.email, | ||
| 'password reset', | ||
| `go to link http://localhost:3000/reset/${token}`, | ||
| ); | ||
|
|
||
| await user.save(); | ||
| res.send('email sent'); | ||
| }; | ||
|
|
||
| const resetPasswordConfirm = async (req, res) => { | ||
| const { resetToken } = req.params; | ||
| const { password, repeatPassword } = req.body; | ||
|
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 requirements specify that the password reset confirmation should expect |
||
|
|
||
| const errorPassword = validatePassword(password); | ||
|
|
||
| if (errorPassword) { | ||
| return res.status(400).send(errorPassword); | ||
| } | ||
|
|
||
| if (password !== repeatPassword) { | ||
| res.send('passwords not equil'); | ||
| } | ||
|
|
||
| const user = await User.findOne({ where: { resetToken } }); | ||
|
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. You should check if a user was found with the provided |
||
|
|
||
| user.password = await bcrypt.hash(password, 10); | ||
| user.resetToken = null; | ||
| user.save(); | ||
| res.send('password changed'); | ||
|
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 showing a success page with a link to login. Simply sending the text 'password changed' is not sufficient. Consider redirecting to a success page or returning a more structured response that the client-side can use to render the correct view. |
||
| }; | ||
|
|
||
| const changeEmail = async (req, res) => { | ||
| const userId = req.user.id; | ||
| const { password, email } = req.body; | ||
|
|
||
| const user = await User.findByPk(userId); | ||
|
|
||
| const isValid = await bcrypt.compare(password, user.password); | ||
|
|
||
| if (!isValid) { | ||
| res.status(401).send('Invalid credentials'); | ||
|
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. You're missing |
||
| } | ||
|
|
||
| const errorEmail = validateEmail(email); | ||
|
|
||
| if (errorEmail) { | ||
| return res.status(400).send(errorEmail); | ||
| } | ||
|
|
||
| const uuid = uuidv4(); | ||
|
|
||
| await send( | ||
| user.email, | ||
| 'Activate email', | ||
| `go to link http://localhost:3000/activation/${uuid}`, | ||
| ); | ||
|
Comment on lines
+131
to
+141
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 implementation for changing an email doesn't fully meet the requirements:
|
||
|
|
||
| user.email = email; | ||
| user.isActivated = false; | ||
| user.activationToken = uuid; | ||
| await user.save(); | ||
| res.redirect('http://localhost:3000/login'); | ||
| }; | ||
|
|
||
| const logout = async (req, res) => { | ||
| res.clearCookie('token'); | ||
| res.redirect('/login'); | ||
| }; | ||
|
|
||
| module.exports = { | ||
| userController: { | ||
| profile, | ||
| changeName, | ||
| changePassword, | ||
| changeEmail, | ||
| logout, | ||
| resetPassword, | ||
| resetPasswordConfirm, | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,32 @@ | ||
| /* eslint-disable max-len */ | ||
| /* eslint-disable no-console */ | ||
| 'use strict'; | ||
|
|
||
| const express = require('express'); | ||
| const cookieParser = require('cookie-parser'); | ||
| const { sequelize } = require('../src/config/database'); // import connect to database | ||
| const { router: authRouter } = require('../src/routes/auth.routes'); | ||
| const { router: userRouter } = require('../src/routes/user.routes'); // import routes | ||
|
|
||
| async function start() { | ||
| try { | ||
| await sequelize.authenticate(); // connect to database | ||
| console.log('database connected'); | ||
|
|
||
| await sequelize.sync({ force: true }); // create tables | ||
|
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. Using |
||
| console.log('tables created'); | ||
|
|
||
| const app = express(); // create express app | ||
|
|
||
| app.use(cookieParser()); | ||
| app.use(express.json()); // allow json in body | ||
| app.use(authRouter); | ||
| app.use(userRouter); | ||
|
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 you to return a 404 error for any pages that are not defined by your routers. You should add a catch-all middleware here, after all other routes, to handle requests to non-existent pages. |
||
|
|
||
| app.listen(3000, () => console.log('server running on 3000 port')); // start server | ||
| } catch (err) { | ||
| console.error('unable connect to database', err); | ||
| } | ||
| } | ||
|
|
||
| start(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| const jwt = require('jsonwebtoken'); | ||
|
|
||
| require('dotenv').config(); | ||
|
|
||
| function authMiddleware(req, res, next) { | ||
| const token = req.cookies.token; | ||
|
|
||
| if (!token) { | ||
| return res.status(401).send('unautorised'); | ||
|
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. There's a small typo here. It should be "unauthorized". |
||
| } | ||
|
|
||
| try { | ||
| const userData = jwt.verify(token, process.env.JWT_SECRET); | ||
|
|
||
| req.user = userData; | ||
| next(); | ||
| } catch { | ||
| return res.status(401).send('Invalid token'); | ||
| } | ||
| } | ||
|
|
||
| function guestMiddleware(req, res, next) { | ||
| const token = req.cookies.token; | ||
|
|
||
| if (token) { | ||
| return res.redirect('/profile'); | ||
| } | ||
|
|
||
| next(); | ||
| } | ||
|
|
||
| module.exports = { authMiddleware, guestMiddleware }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| const { DataTypes } = require('sequelize'); | ||
| const { sequelize } = require('../config/database'); | ||
|
|
||
| const Users = sequelize.define('Users', { | ||
| name: { | ||
| type: DataTypes.STRING, | ||
| allowNull: false, | ||
| }, | ||
| email: { | ||
| type: DataTypes.STRING, | ||
| allowNull: false, | ||
| unique: true, | ||
| }, | ||
| password: { | ||
| type: DataTypes.STRING, | ||
| allowNull: false, | ||
| }, | ||
| isActivated: { | ||
| type: DataTypes.BOOLEAN, | ||
| allowNull: true, | ||
| defaultValue: false, | ||
| }, | ||
| activationToken: { | ||
| type: DataTypes.STRING, | ||
| allowNull: true, | ||
| }, | ||
| resetToken: { | ||
| type: DataTypes.STRING, | ||
| allowNull: true, | ||
| }, | ||
| }); | ||
|
|
||
| module.exports = Users; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| /* eslint-disable no-unused-vars */ | ||
| const express = require('express'); | ||
| const router = express.Router(); | ||
| const User = require('../models/user.model'); | ||
|
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 { authController } = require('../controllers/auth.controller'); | ||
| const { | ||
| authMiddleware, | ||
|
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 |
||
| guestMiddleware, | ||
| } = require('../middlewares/auth.middleware'); | ||
|
|
||
| router.post('/registration', guestMiddleware, authController.registration); | ||
|
|
||
| router.get( | ||
| '/activation/:activationToken', | ||
| guestMiddleware, | ||
| authController.activation, | ||
| ); | ||
| router.post('/login', guestMiddleware, authController.login); | ||
|
|
||
| module.exports = { router }; | ||
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.
While
res.send(201)works, it's more idiomatic in Express to be explicit with your status codes. Consider usingres.sendStatus(201)orres.status(201).send('User created'). This makes the code's intent clearer.