From 1cc457c7a992a123a12e46d1cc52861b2f33d7d0 Mon Sep 17 00:00:00 2001 From: in7264 Date: Tue, 17 Mar 2026 09:45:40 +0200 Subject: [PATCH 1/3] Solution --- src/config/database.js | 16 ++++ src/controllers/auth.controller.js | 106 +++++++++++++++++++++ src/controllers/user.controller.js | 147 +++++++++++++++++++++++++++++ src/index.js | 31 ++++++ src/middlewares/auth.middleware.js | 32 +++++++ src/models/user.model.js | 33 +++++++ src/routes/auth.routes.js | 21 +++++ src/routes/user.routes.js | 30 ++++++ src/services/auth.services.js | 48 ++++++++++ 9 files changed, 464 insertions(+) create mode 100644 src/config/database.js create mode 100644 src/controllers/auth.controller.js create mode 100644 src/controllers/user.controller.js create mode 100644 src/middlewares/auth.middleware.js create mode 100644 src/models/user.model.js create mode 100644 src/routes/auth.routes.js create mode 100644 src/routes/user.routes.js create mode 100644 src/services/auth.services.js 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..337e519e --- /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.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'); + } + + 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) { + res.send('User not found'); + } + + const isValid = await bcrypt.compare(password, existUser.password); + + if (!isValid) { + res.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..c1e2e346 --- /dev/null +++ b/src/controllers/user.controller.js @@ -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; + + if (!confirm) { + 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'); + } + + 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) { + res.send('passwords not equil'); + } + + const user = await User.findOne({ where: { resetToken } }); + + user.password = await bcrypt.hash(password, 10); + user.resetToken = null; + user.save(); + res.send('password changed'); +}; + +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'); + } + + 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}`, + ); + + 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, + }, +}; diff --git a/src/index.js b/src/index.js index ad9a93a7..899d2767 100644 --- a/src/index.js +++ b/src/index.js @@ -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 + 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); + + 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..51e8551c --- /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('unautorised'); + } + + 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..cfd60dd2 --- /dev/null +++ b/src/routes/auth.routes.js @@ -0,0 +1,21 @@ +/* eslint-disable no-unused-vars */ +const express = require('express'); +const router = express.Router(); +const User = require('../models/user.model'); + +const { authController } = require('../controllers/auth.controller'); +const { + authMiddleware, + 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..377227fa --- /dev/null +++ b/src/routes/user.routes.js @@ -0,0 +1,30 @@ +/* eslint-disable no-unused-vars */ +const express = require('express'); +const router = express.Router(); +const User = require('../models/user.model'); + +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, +}; From dbc81da981248bd8c08d42a53f3b4e6bc2c5dbe9 Mon Sep 17 00:00:00 2001 From: in7264 Date: Tue, 17 Mar 2026 10:11:09 +0200 Subject: [PATCH 2/3] Solution --- src/controllers/auth.controller.js | 6 ++--- src/controllers/user.controller.js | 39 +++++++++++++++++++++--------- src/index.js | 8 +++++- src/routes/auth.routes.js | 7 +----- 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 337e519e..01e6d0ea 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -53,7 +53,7 @@ const activation = async (req, res) => { const user = await User.findOne({ where: { activationToken } }); if (!user) { - res.status(404).send('invalind activation token'); + return res.status(404).send('invalind activation token'); } user.isActivated = true; @@ -68,13 +68,13 @@ const login = async (req, res) => { const existUser = await User.findOne({ where: { email } }); if (!existUser) { - res.send('User not found'); + return res.send('User not found'); } const isValid = await bcrypt.compare(password, existUser.password); if (!isValid) { - res.send('Invalid credentials'); + return res.send('Invalid credentials'); } if (!existUser.isActivated) { diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index c1e2e346..47c522be 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -34,8 +34,8 @@ const changePassword = async (req, res) => { const userId = req.user.id; const { password, newPassword, confirm } = req.body; - if (!confirm) { - res.send('You need confirm change'); + if (newPassword !== confirm) { + return res.send('Passwords do not match'); } const user = await User.findByPk(userId); @@ -43,7 +43,7 @@ const changePassword = async (req, res) => { const isValid = await bcrypt.compare(password, user.password); if (!isValid) { - res.status(401).send('Invalid credentials'); + return res.status(401).send('Invalid credentials'); } user.password = await bcrypt.hash(newPassword, 10); @@ -85,27 +85,36 @@ const resetPasswordConfirm = async (req, res) => { } if (password !== repeatPassword) { - res.send('passwords not equil'); + 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; - user.save(); - res.send('password changed'); + await user.save(); + + return res.redirect('/login'); }; const changeEmail = async (req, res) => { const userId = req.user.id; - const { password, email } = req.body; + const { password, email, confirmEmail } = req.body; const user = await User.findByPk(userId); const isValid = await bcrypt.compare(password, user.password); if (!isValid) { - res.status(401).send('Invalid credentials'); + return res.status(401).send('Invalid credentials'); + } + + if (email !== confirmEmail) { + return res.status(400).send('Emails do not match'); } const errorEmail = validateEmail(email); @@ -118,15 +127,23 @@ const changeEmail = async (req, res) => { await send( user.email, - 'Activate email', - `go to link http://localhost:3000/activation/${uuid}`, + '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(); - res.redirect('http://localhost:3000/login'); + + return res.redirect('/login'); }; const logout = async (req, res) => { diff --git a/src/index.js b/src/index.js index 899d2767..9b1ca46e 100644 --- a/src/index.js +++ b/src/index.js @@ -19,10 +19,16 @@ async function start() { const app = express(); // create express app app.use(cookieParser()); - app.use(express.json()); // allow json in body + 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); diff --git a/src/routes/auth.routes.js b/src/routes/auth.routes.js index cfd60dd2..9fd3f93a 100644 --- a/src/routes/auth.routes.js +++ b/src/routes/auth.routes.js @@ -1,13 +1,8 @@ -/* eslint-disable no-unused-vars */ const express = require('express'); const router = express.Router(); -const User = require('../models/user.model'); const { authController } = require('../controllers/auth.controller'); -const { - authMiddleware, - guestMiddleware, -} = require('../middlewares/auth.middleware'); +const { guestMiddleware } = require('../middlewares/auth.middleware'); router.post('/registration', guestMiddleware, authController.registration); From da255fa656b6e6e8a06b8c78129a9e138babad1b Mon Sep 17 00:00:00 2001 From: in7264 Date: Tue, 17 Mar 2026 10:27:04 +0200 Subject: [PATCH 3/3] Solution --- src/controllers/auth.controller.js | 8 ++++---- src/controllers/user.controller.js | 5 ++++- src/index.js | 2 +- src/middlewares/auth.middleware.js | 2 +- src/routes/user.routes.js | 1 - 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/controllers/auth.controller.js b/src/controllers/auth.controller.js index 01e6d0ea..0ebfdf37 100644 --- a/src/controllers/auth.controller.js +++ b/src/controllers/auth.controller.js @@ -44,7 +44,7 @@ const registration = async (req, res) => { 'Activate email', `go to link http://localhost:3000/activation/${uuid}`, ); - res.send(201); + res.status(201).send('User created'); }; const activation = async (req, res) => { @@ -53,7 +53,7 @@ const activation = async (req, res) => { const user = await User.findOne({ where: { activationToken } }); if (!user) { - return res.status(404).send('invalind activation token'); + return res.status(404).send('Invalid activation token'); } user.isActivated = true; @@ -68,13 +68,13 @@ const login = async (req, res) => { const existUser = await User.findOne({ where: { email } }); if (!existUser) { - return res.send('User not found'); + return res.status(404).send('User not found'); } const isValid = await bcrypt.compare(password, existUser.password); if (!isValid) { - return res.send('Invalid credentials'); + return res.status(401).send('Invalid credentials'); } if (!existUser.isActivated) { diff --git a/src/controllers/user.controller.js b/src/controllers/user.controller.js index 47c522be..f5f4592d 100644 --- a/src/controllers/user.controller.js +++ b/src/controllers/user.controller.js @@ -98,7 +98,10 @@ const resetPasswordConfirm = async (req, res) => { user.resetToken = null; await user.save(); - return res.redirect('/login'); + return res.send(` +

Password changed successfully

+ Go to login +`); }; const changeEmail = async (req, res) => { diff --git a/src/index.js b/src/index.js index 9b1ca46e..046d24b8 100644 --- a/src/index.js +++ b/src/index.js @@ -13,7 +13,7 @@ async function start() { await sequelize.authenticate(); // connect to database console.log('database connected'); - await sequelize.sync({ force: true }); // create tables + await sequelize.sync({ alter: true }); // create tables console.log('tables created'); const app = express(); // create express app diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index 51e8551c..62ea70f9 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -6,7 +6,7 @@ function authMiddleware(req, res, next) { const token = req.cookies.token; if (!token) { - return res.status(401).send('unautorised'); + return res.status(401).send('unauthorized'); } try { diff --git a/src/routes/user.routes.js b/src/routes/user.routes.js index 377227fa..6f44719e 100644 --- a/src/routes/user.routes.js +++ b/src/routes/user.routes.js @@ -1,7 +1,6 @@ /* eslint-disable no-unused-vars */ const express = require('express'); const router = express.Router(); -const User = require('../models/user.model'); const { userController } = require('../controllers/user.controller'); const {