diff --git a/src/config/database.js b/src/config/database.js new file mode 100644 index 00000000..45ae4e94 --- /dev/null +++ b/src/config/database.js @@ -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 }; diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js new file mode 100644 index 00000000..0ebfdf37 --- /dev/null +++ b/src/controllers/auth.controller.js @@ -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.status(201).send('User created'); +}; + +const activation = async (req, res) => { + const { activationToken } = req.params; + + const user = await User.findOne({ where: { activationToken } }); + + if (!user) { + return res.status(404).send('Invalid activation token'); + } + + user.isActivated = true; + user.activationToken = null; + await user.save(); + res.redirect('http://localhost:3000/profile'); +}; + +const login = async (req, res) => { + const { email, password } = req.body; + + const existUser = await User.findOne({ where: { email } }); + + if (!existUser) { + return res.status(404).send('User not found'); + } + + const isValid = await bcrypt.compare(password, existUser.password); + + if (!isValid) { + return res.status(401).send('Invalid credentials'); + } + + 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, + }, +}; diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js new file mode 100644 index 00000000..f5f4592d --- /dev/null +++ b/src/controllers/user.controller.js @@ -0,0 +1,167 @@ +/* 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; + + if (newPassword !== confirm) { + return res.send('Passwords do not match'); + } + + const user = await User.findByPk(userId); + + const isValid = await bcrypt.compare(password, user.password); + + if (!isValid) { + return res.status(401).send('Invalid credentials'); + } + + 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; + + const errorPassword = validatePassword(password); + + if (errorPassword) { + return res.status(400).send(errorPassword); + } + + if (password !== repeatPassword) { + return res.send('passwords not equil'); + } + + const user = await User.findOne({ where: { resetToken } }); + + if (!user) { + return res.status(401).send('Invalid credentials'); + } + + user.password = await bcrypt.hash(password, 10); + user.resetToken = null; + await user.save(); + + return res.send(` +

Password changed successfully

+ Go to login +`); +}; + +const changeEmail = async (req, res) => { + const userId = req.user.id; + const { password, email, confirmEmail } = req.body; + + const user = await User.findByPk(userId); + + const isValid = await bcrypt.compare(password, user.password); + + if (!isValid) { + return res.status(401).send('Invalid credentials'); + } + + if (email !== confirmEmail) { + return res.status(400).send('Emails do not match'); + } + + const errorEmail = validateEmail(email); + + if (errorEmail) { + return res.status(400).send(errorEmail); + } + + const uuid = uuidv4(); + + await send( + user.email, + 'Email change notification', + 'Your email is being changed.', + ); + + await send( + email, + 'Activate new email', + `Go to link http://localhost:3000/activation/${uuid}`, + ); + + user.email = email; + user.isActivated = false; + user.activationToken = uuid; + + await user.save(); + + return res.redirect('/login'); +}; + +const logout = async (req, res) => { + res.clearCookie('token'); + res.redirect('/login'); +}; + +module.exports = { + userController: { + profile, + changeName, + changePassword, + changeEmail, + logout, + resetPassword, + resetPasswordConfirm, + }, +}; diff --git a/src/index.js b/src/index.js index ad9a93a7..046d24b8 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,38 @@ +/* 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({ alter: true }); // create tables + console.log('tables created'); + + const app = express(); // create express app + + app.use(cookieParser()); + app.use(express.json()); + + app.use(authRouter); + app.use(userRouter); + + // 404 handler + app.use((req, res) => { + res.status(404).send('Page not found'); + }); + + app.listen(3000, () => console.log('server running on 3000 port')); // start server + } catch (err) { + console.error('unable connect to database', err); + } +} + +start(); diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js new file mode 100644 index 00000000..62ea70f9 --- /dev/null +++ b/src/middlewares/auth.middleware.js @@ -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('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 }; diff --git a/src/models/user.model.js b/src/models/user.model.js new file mode 100644 index 00000000..1dc1510f --- /dev/null +++ b/src/models/user.model.js @@ -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; diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js new file mode 100644 index 00000000..9fd3f93a --- /dev/null +++ b/src/routes/auth.routes.js @@ -0,0 +1,16 @@ +const express = require('express'); +const router = express.Router(); + +const { authController } = require('../controllers/auth.controller'); +const { 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 }; diff --git a/src/routes/user.routes.js b/src/routes/user.routes.js new file mode 100644 index 00000000..6f44719e --- /dev/null +++ b/src/routes/user.routes.js @@ -0,0 +1,29 @@ +/* eslint-disable no-unused-vars */ +const express = require('express'); +const router = express.Router(); + +const { userController } = require('../controllers/user.controller'); +const { + authMiddleware, + guestMiddleware, +} = require('../middlewares/auth.middleware'); + +router.get('/profile', authMiddleware, userController.profile); +router.patch('/profile/name', authMiddleware, userController.changeName); + +router.patch( + '/profile/password', + authMiddleware, + userController.changePassword, +); +router.patch('/profile/email', authMiddleware, userController.changeEmail); +router.post('/logout', authMiddleware, userController.logout); +router.post('/reset', guestMiddleware, userController.resetPassword); + +router.post( + '/reset/:resetToken', + guestMiddleware, + userController.resetPasswordConfirm, +); + +module.exports = { router }; diff --git a/src/services/auth.services.js b/src/services/auth.services.js new file mode 100644 index 00000000..c44b404f --- /dev/null +++ b/src/services/auth.services.js @@ -0,0 +1,48 @@ +const nodemailer = require('nodemailer'); + +require('dotenv').config(); + +function validateEmail(email) { + const emailPattern = /^[\w.+-]+@([\w-]+\.){1,3}[\w-]{2,}$/; + + if (!email) { + return 'Email is required'; + } + + if (!emailPattern.test(email)) { + return 'Email is not valid'; + } +} + +function validatePassword(password) { + if (!password) { + return 'Password is required'; + } + + if (password.length < 6) { + return 'At least 6 characters'; + } +} + +const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.NODEMAILER_LOGIN, + pass: process.env.NODEMAILER_PASSWORD, + }, +}); + +function send(email, subject, html) { + return transporter.sendMail({ + from: 'Auth API', + to: email, + subject, + html, + }); +} + +module.exports = { + validateEmail, + validatePassword, + send, +};