From e256bbf492ea521eb7ac39f4ec61200967d91d9a Mon Sep 17 00:00:00 2001 From: Gianluca Marchese Date: Thu, 5 Feb 2026 23:48:35 +0000 Subject: [PATCH 1/3] node_auth --- .github/workflows/test.yml-template | 23 +++ .gitignore | 4 - package-lock.json | 9 +- package.json | 2 +- src/controller/auth.controller.js | 226 ++++++++++++++++++++++++++++ src/controller/user.controller.js | 108 +++++++++++++ src/index.js | 25 ++- src/middlewares/auth.middleware.js | 20 +++ src/models/Token.model.js | 21 +++ src/models/User.model.js | 40 +++++ src/router/auth.router.js | 13 ++ src/router/user.router.js | 10 ++ src/services/email.services.js | 45 ++++++ src/services/jwt.services.js | 32 ++++ src/services/user.services.js | 36 +++++ src/util/db.js | 14 ++ 16 files changed, 618 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/test.yml-template create mode 100644 src/controller/auth.controller.js create mode 100644 src/controller/user.controller.js create mode 100644 src/middlewares/auth.middleware.js create mode 100644 src/models/Token.model.js create mode 100644 src/models/User.model.js create mode 100644 src/router/auth.router.js create mode 100644 src/router/user.router.js create mode 100644 src/services/email.services.js create mode 100644 src/services/jwt.services.js create mode 100644 src/services/user.services.js create mode 100644 src/util/db.js diff --git a/.github/workflows/test.yml-template b/.github/workflows/test.yml-template new file mode 100644 index 00000000..bb13dfc4 --- /dev/null +++ b/.github/workflows/test.yml-template @@ -0,0 +1,23 @@ +name: Test + +on: + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/.gitignore b/.gitignore index ed48a299..bd6a178a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,3 @@ node_modules # MacOS .DS_Store - -# env files -*.env -.env* diff --git a/package-lock.json b/package-lock.json index 288d83dd..f67c20c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "GPL-3.0", "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", @@ -1467,10 +1467,11 @@ } }, "node_modules/@mate-academy/scripts": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-1.8.6.tgz", - "integrity": "sha512-b4om/whj4G9emyi84ORE3FRZzCRwRIesr8tJHXa8EvJdOaAPDpzcJ8A0sFfMsWH9NUOVmOwkBtOXDu5eZZ00Ig==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@mate-academy/scripts/-/scripts-2.1.3.tgz", + "integrity": "sha512-a07wHTj/1QUK2Aac5zHad+sGw4rIvcNl5lJmJpAD7OxeSbnCdyI6RXUHwXhjF5MaVo9YHrJ0xVahyERS2IIyBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/rest": "^17.11.2", "@types/get-port": "^4.2.0", diff --git a/package.json b/package.json index 5e195a15..9d160819 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "license": "GPL-3.0", "devDependencies": { "@mate-academy/eslint-config": "latest", - "@mate-academy/scripts": "^1.8.6", + "@mate-academy/scripts": "^2.1.3", "eslint": "^8.57.0", "eslint-plugin-jest": "^28.6.0", "eslint-plugin-node": "^11.1.0", diff --git a/src/controller/auth.controller.js b/src/controller/auth.controller.js new file mode 100644 index 00000000..c9559b70 --- /dev/null +++ b/src/controller/auth.controller.js @@ -0,0 +1,226 @@ +/* eslint-disable no-useless-return */ +import { userServices } from '../services/user.services.js'; +import { emailServices } from '../services/email.services.js'; +import { jwtService } from '../services/jwt.services.js'; +import bcrypt, { compare } from 'bcrypt'; +import { v4 as uuidv4 } from 'uuid'; +import { User } from '../models/User.model.js'; + +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'; + } +} + +function validateName(value) { + if (!value) { + return 'Name is required'; + } + + if (value.trim().length < 4) { + return 'Name length must be more than 4 symbols'; + } +} + +function validatePassword(value) { + if (!value) { + return 'Password is required'; + } + + if (value.length < 6) { + return 'At least 6 characters'; + } +} + +const registerUser = async (req, res) => { + try { + const { name, email, password } = req.body; + const activationToken = uuidv4(); + + if (!name || !email || !password) { + res.status(400).json({ message: 'All fields are required' }); + + return; + } + + const errors = { + email: validateEmail(email), + password: validatePassword(password), + name: validateName(name), + }; + + if (errors.email || errors.password || errors.name) { + res.status(400).json(errors); + + return; + } + + const hashPassword = bcrypt.hashSync(password, 10); + + await userServices.registerUser(name, email, hashPassword, activationToken); + + await emailServices.sendActivationEmail(email, activationToken); + + res + .status(201) + .json({ message: 'User registered. Check your email for activation.' }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +const activateUser = async (req, res) => { + const { activationToken } = req.params; + + const user = await User.findOne({ + where: { activationToken }, + }); + + if (!user) { + res.status(404).json({ message: 'User not found' }); + + return; + } + + user.activationToken = null; + await user.save(); + + res.redirect('/profile'); +}; + +const loginUser = async (req, res) => { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).send({ message: 'All fields are required' }); + } + + const user = await userServices.findUser(email); + + if (!user) { + res.status(401).json({ message: 'User not found' }); + + return; + } + + if (user.activationToken !== null) { + res.status(403).json({ message: 'Please activate your email' }); + + return; + } + + const isPasswordValid = await compare(password, user.password); + + if (!isPasswordValid) { + res.status(401).json({ message: 'Invalid credentials' }); + + return; + } + + await generateTokens(res, user); + res.redirect('/profile'); +}; + +const refresh = (req, res) => { + const { refreshToken } = req.cookies; + + const user = jwtService.verifyRefresh(refreshToken); + + if (!user) { + res.sendStatus(401); + + return; + } + + generateTokens(res, user); +}; + +const generateTokens = async (res, user) => { + const normilizeUser = userServices.normilizeUser(user); + const accessToken = jwtService.sign(normilizeUser); + const refreshToken = jwtService.signRefresh(normilizeUser); + + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: true, + maxAge: 30 * 24 * 60 * 60 * 1000, + }); + + res.send({ + user: normilizeUser, + accessToken, + }); +}; + +const logout = (req, res) => { + res.clearCookie('refreshToken').redirect('/login'); +}; + +const forgot = async (req, res) => { + const { email } = req.body; + + if (!email) { + res.status(400).json({ message: 'Email is required' }); + + return; + } + + const user = await userServices.findUser(email); + + if (!user) { + res.sendStatus(200); + + return; + } + + const resetToken = uuidv4(); + + user.resetToken = resetToken; + + await user.save(); + await emailServices.sendResetPasswordEmail(email, resetToken); + + res.send({ message: 'Password reset email sent' }); +}; + +const resetPassword = async (req, res) => { + const { resetToken } = req.params; + const { password, confirmation } = req.body; + + if (!password || password !== confirmation) { + res.status(400).json({ message: 'Passwords do not match' }); + + return; + } + + const user = await User.findOne({ where: { resetToken } }); + + if (!user) { + res.status(400).json({ message: 'Invalid reset token' }); + + return; + } + + user.password = bcrypt.hashSync(password, 10); + user.resetToken = null; + + await user.save(); + + res.send({ message: 'Password successfully changed' }); +}; + +export const authController = { + registerUser, + activateUser, + loginUser, + refresh, + logout, + forgot, + resetPassword, +}; diff --git a/src/controller/user.controller.js b/src/controller/user.controller.js new file mode 100644 index 00000000..1c8a541c --- /dev/null +++ b/src/controller/user.controller.js @@ -0,0 +1,108 @@ +import { User } from '../models/User.model.js'; +import { userServices } from '../services/user.services.js'; +import { emailServices } from '../services/email.services.js'; +import bcrypt from 'bcrypt'; + +const getAllUsers = async (req, res) => { + const users = await User.findAll(); + + res.send(users); +}; + +const getUserById = async (req, res) => { + const { userId } = req.params; + + const user = await userServices.findUserById(userId); + + res.send(user); +}; + +const updateName = async (req, res) => { + try { + const userId = req.user.userId; + const { name } = req.body; + + if (!name || !name.trim()) { + res.status(400).json({ message: 'Name is required' }); + + return; + } + + const user = await userServices.updateNameService(userId, name); + + res.send(user); + } catch (error) { + res.status(500).send(error); + } +}; + +const updatePassword = async (req, res) => { + const { oldPassword, newPassword, confirmation } = req.body; + const userId = req.user.userId; + + if (newPassword !== confirmation) { + res.status(400).json({ message: 'Passwords do not match' }); + + return; + } + + const user = await userServices.findUserById(userId); + const isValid = await bcrypt.compare(oldPassword, user.password); + + if (!isValid) { + res.status(401).json({ message: 'Old password is incorrect' }); + + return; + } + + user.password = bcrypt.hashSync(newPassword, 10); + await user.save(); + + res.send({ message: 'Password updated successfully' }); +}; + +const updateEmail = async (req, res) => { + const { password, newEmail, confirmation } = req.body; + const userId = req.user.userId; + + if (!newEmail || !confirmation) { + res + .status(400) + .json({ message: 'New email and confirmation are required' }); + + return; + } + + if (newEmail !== confirmation) { + res.status(400).json({ message: 'Emails do not match' }); + + return; + } + + const user = await userServices.findUserById(userId); + + const isValidPassword = await bcrypt.compare(password, user.password); + + if (!isValidPassword) { + res.status(401).json({ message: 'Invalid password' }); + + return; + } + + const oldEmail = user.email; + + user.email = newEmail; + + await user.save(); + await emailServices.sendEmailChangedNotification(oldEmail, newEmail); + + res.send({ message: 'Email updated successfully' }); +}; + +export const userController = { + getAllUsers, + getUserById, + updateName, + updateEmail, + updatePassword, +}; diff --git a/src/index.js b/src/index.js index ad9a93a7..bdda261d 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1,24 @@ -'use strict'; +import express from 'express'; +import cookieParser from 'cookie-parser'; + +import authRouter from './router/auth.router.js'; +import userRouter from './router/user.router.js'; +import { authMiddleware } from './middlewares/auth.middleware.js'; + +const app = express(); +const port = process.env.PORT || 3000; + +app.use(express.json()); +app.use(cookieParser()); + +app.use('/', authRouter); +app.use('/user', authMiddleware, userRouter); + +app.use('*', (req, res) => { + res.status(404).json({ message: 'Not found' }); +}); + +app.listen(port, () => { + // eslint-disable-next-line no-console + console.log(`Server running at http://localhost:${port}`); +}); diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js new file mode 100644 index 00000000..6a4dccc5 --- /dev/null +++ b/src/middlewares/auth.middleware.js @@ -0,0 +1,20 @@ +import { jwtService } from '../services/jwt.services.js'; + +export const authMiddleware = (req, res, next) => { + const auth = req.headers.authorization; + + if (!auth) { + return res.sendStatus(401); + } + + const [, token] = auth.split(' '); + + const user = jwtService.verify(token); + + if (!user) { + return res.sendStatus(401); + } + + req.user = user; + next(); +}; diff --git a/src/models/Token.model.js b/src/models/Token.model.js new file mode 100644 index 00000000..b0cfe255 --- /dev/null +++ b/src/models/Token.model.js @@ -0,0 +1,21 @@ +import { DataTypes } from 'sequelize'; +import client from '../util/db.js'; +import { User } from './User.model.js'; + +export const Token = client.define( + 'Token', + { + refreshToken: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + tableName: 'tokens', + timestamps: true, + underscored: true, + }, +); + +Token.belongsTo(User, { foreignKey: 'user_id', onDelete: 'CASCADE' }); +User.hasOne(Token, { foreignKey: 'user_id' }); diff --git a/src/models/User.model.js b/src/models/User.model.js new file mode 100644 index 00000000..562e363a --- /dev/null +++ b/src/models/User.model.js @@ -0,0 +1,40 @@ +import { DataTypes } from 'sequelize'; +import client from '../util/db.js'; + +export const User = client.define( + 'User', + { + userId: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + field: 'user_id', + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + email: { + type: DataTypes.STRING, + allowNull: false, + unique: true, + }, + password: { + type: DataTypes.STRING, + allowNull: false, + }, + activationToken: { + type: DataTypes.STRING, + field: 'activation_token', + }, + resetToken: { + type: DataTypes.STRING, + field: 'reset_token', + }, + }, + { + tableName: 'users', + timestamps: true, + underscored: true, + }, +); diff --git a/src/router/auth.router.js b/src/router/auth.router.js new file mode 100644 index 00000000..f5e09e8c --- /dev/null +++ b/src/router/auth.router.js @@ -0,0 +1,13 @@ +import express from 'express'; +import { authController } from '../controller/auth.controller.js'; + +const router = express.Router(); + +router.post('/auth', authController.registerUser); +router.get('/activate/:activationToken', authController.activateUser); +router.post('/login', authController.loginUser); +router.get('/logout', authController.logout); +router.post('/forgot', authController.forgot); +router.post('/password-reset/:resetToken', authController.resetPassword); + +export default router; diff --git a/src/router/user.router.js b/src/router/user.router.js new file mode 100644 index 00000000..2b6a5e85 --- /dev/null +++ b/src/router/user.router.js @@ -0,0 +1,10 @@ +import express from 'express'; +import { userController } from '../controller/user.controller.js'; + +const router = express.Router(); + +router.patch('/name', userController.updateName); +router.patch('/email', userController.updateEmail); +router.patch('/password', userController.updatePassword); + +export default router; diff --git a/src/services/email.services.js b/src/services/email.services.js new file mode 100644 index 00000000..99d6623b --- /dev/null +++ b/src/services/email.services.js @@ -0,0 +1,45 @@ +import 'dotenv/config'; +import nodemailer from 'nodemailer'; + +const transporter = nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: process.env.SMTP_PORT, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASSWORD, + }, +}); + +function sendEmail(to, subject, html) { + return transporter.sendMail({ to, subject, html }); +} + +function sendActivationEmail(email, token) { + const href = `${process.env.CLIENT_HOST}/activate/${token}`; + + return sendEmail( + email, + 'Account activation', + `Activate your account: ${href}`, + ); +} + +function sendResetPasswordEmail(email, token) { + const href = `${process.env.CLIENT_HOST}/password-reset/${token}`; + + return sendEmail(email, 'Reset Password', `${href}`); +} + +function sendEmailChangedNotification(oldEmail, newEmail) { + return sendEmail( + oldEmail, + 'Email changed', + `Your email was changed to ${newEmail}`, + ); +} + +export const emailServices = { + sendActivationEmail, + sendResetPasswordEmail, + sendEmailChangedNotification, +}; diff --git a/src/services/jwt.services.js b/src/services/jwt.services.js new file mode 100644 index 00000000..5258c36d --- /dev/null +++ b/src/services/jwt.services.js @@ -0,0 +1,32 @@ +import jwt from 'jsonwebtoken'; + +function sign(user) { + return jwt.sign(user, process.env.JWT_SECRET, { expiresIn: '1d' }); +} + +function verify(token) { + try { + return jwt.verify(token, process.env.JWT_SECRET); + } catch { + return null; + } +} + +function signRefresh(user) { + return jwt.sign(user, process.env.JWT_REFRESH_SECRET, { expiresIn: '1d' }); +} + +function verifyRefresh(token) { + try { + return jwt.verify(token, process.env.JWT_REFRESH_SECRET); + } catch { + return null; + } +} + +export const jwtService = { + sign, + verify, + signRefresh, + verifyRefresh, +}; diff --git a/src/services/user.services.js b/src/services/user.services.js new file mode 100644 index 00000000..89c5e199 --- /dev/null +++ b/src/services/user.services.js @@ -0,0 +1,36 @@ +import { User } from '../models/User.model.js'; + +const registerUser = (name, email, password, activationToken) => + User.create({ + name, + email, + password, + activationToken, + }); + +const findUser = (email) => User.findOne({ where: { email } }); + +const findUserById = (id) => User.findByPk(id); + +const updateNameService = async (id, name) => { + const user = await findUserById(id); + + user.name = name; + await user.save(); + + return user; +}; + +const normilizeUser = (user) => ({ + userId: user.userId, + email: user.email, + name: user.name, +}); + +export const userServices = { + registerUser, + findUser, + findUserById, + updateNameService, + normilizeUser, +}; diff --git a/src/util/db.js b/src/util/db.js new file mode 100644 index 00000000..ab1b0f57 --- /dev/null +++ b/src/util/db.js @@ -0,0 +1,14 @@ +import { Sequelize } from 'sequelize'; +import 'dotenv/config'; + +const client = new Sequelize( + process.env.POSTGRES_DB, + process.env.POSTGRES_USER, + process.env.POSTGRES_PASSWORD, + { + host: process.env.POSTGRES_HOST, + dialect: 'postgres', + }, +); + +export default client; From a8e0ed0b2c9d24632461b825b0a3a02528e4b6fb Mon Sep 17 00:00:00 2001 From: Gianluca Marchese Date: Thu, 5 Feb 2026 23:57:51 +0000 Subject: [PATCH 2/3] Fix user services and complete auth flow --- src/controller/auth.controller.js | 173 +++++++--------------------- src/controller/user.controller.js | 118 +++++++++---------- src/middlewares/guest.middleware.js | 18 +++ src/router/auth.router.js | 25 +++- src/services/user.services.js | 29 +++-- 5 files changed, 156 insertions(+), 207 deletions(-) create mode 100644 src/middlewares/guest.middleware.js diff --git a/src/controller/auth.controller.js b/src/controller/auth.controller.js index c9559b70..34705cf5 100644 --- a/src/controller/auth.controller.js +++ b/src/controller/auth.controller.js @@ -1,91 +1,50 @@ -/* eslint-disable no-useless-return */ +import bcrypt from 'bcrypt'; +import { v4 as uuidv4 } from 'uuid'; + import { userServices } from '../services/user.services.js'; import { emailServices } from '../services/email.services.js'; import { jwtService } from '../services/jwt.services.js'; -import bcrypt, { compare } from 'bcrypt'; -import { v4 as uuidv4 } from 'uuid'; import { User } from '../models/User.model.js'; -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'; - } -} - -function validateName(value) { - if (!value) { - return 'Name is required'; - } - - if (value.trim().length < 4) { - return 'Name length must be more than 4 symbols'; - } -} - -function validatePassword(value) { - if (!value) { - return 'Password is required'; - } - - if (value.length < 6) { +function validatePassword(pwd) { + if (!pwd || pwd.length < 6) { return 'At least 6 characters'; } } const registerUser = async (req, res) => { - try { - const { name, email, password } = req.body; - const activationToken = uuidv4(); - - if (!name || !email || !password) { - res.status(400).json({ message: 'All fields are required' }); + const { name, email, password } = req.body; - return; - } - - const errors = { - email: validateEmail(email), - password: validatePassword(password), - name: validateName(name), - }; + if (!name || !email || !password) { + return res.status(400).json({ message: 'All fields are required' }); + } - if (errors.email || errors.password || errors.name) { - res.status(400).json(errors); + if (validatePassword(password)) { + return res.status(400).json({ password: validatePassword(password) }); + } - return; - } + const exists = await userServices.findUser(email); - const hashPassword = bcrypt.hashSync(password, 10); + if (exists) { + return res.status(409).json({ message: 'Email already exists' }); + } - await userServices.registerUser(name, email, hashPassword, activationToken); + const activationToken = uuidv4(); + const hash = bcrypt.hashSync(password, 10); - await emailServices.sendActivationEmail(email, activationToken); + await userServices.registerUser(name, email, hash, activationToken); + await emailServices.sendActivationEmail(email, activationToken); - res - .status(201) - .json({ message: 'User registered. Check your email for activation.' }); - } catch (error) { - res.status(500).json({ message: error.message }); - } + res.status(201).json({ message: 'Check your email to activate account' }); }; const activateUser = async (req, res) => { const { activationToken } = req.params; - const user = await User.findOne({ - where: { activationToken }, - }); + const user = await User.findOne({ where: { activationToken } }); if (!user) { - res.status(404).json({ message: 'User not found' }); - - return; + return res.status(404).json({ message: 'User not found' }); } user.activationToken = null; @@ -98,95 +57,53 @@ const loginUser = async (req, res) => { const { email, password } = req.body; if (!email || !password) { - return res.status(400).send({ message: 'All fields are required' }); + return res.status(400).json({ message: 'All fields are required' }); } const user = await userServices.findUser(email); if (!user) { - res.status(401).json({ message: 'User not found' }); - - return; + return res.status(401).json({ message: 'Invalid credentials' }); } - if (user.activationToken !== null) { - res.status(403).json({ message: 'Please activate your email' }); - - return; + if (user.activationToken) { + return res.status(403).json({ message: 'Activate your email' }); } - const isPasswordValid = await compare(password, user.password); + const valid = await bcrypt.compare(password, user.password); - if (!isPasswordValid) { - res.status(401).json({ message: 'Invalid credentials' }); - - return; + if (!valid) { + return res.status(401).json({ message: 'Invalid credentials' }); } - await generateTokens(res, user); - res.redirect('/profile'); -}; + const normalized = userServices.normilizeUser(user); + const token = jwtService.sign(normalized); -const refresh = (req, res) => { - const { refreshToken } = req.cookies; - - const user = jwtService.verifyRefresh(refreshToken); - - if (!user) { - res.sendStatus(401); - - return; - } - - generateTokens(res, user); -}; - -const generateTokens = async (res, user) => { - const normilizeUser = userServices.normilizeUser(user); - const accessToken = jwtService.sign(normilizeUser); - const refreshToken = jwtService.signRefresh(normilizeUser); - - res.cookie('refreshToken', refreshToken, { - httpOnly: true, - secure: true, - maxAge: 30 * 24 * 60 * 60 * 1000, - }); - - res.send({ - user: normilizeUser, - accessToken, - }); + res.json({ accessToken: token, user: normalized }); }; const logout = (req, res) => { - res.clearCookie('refreshToken').redirect('/login'); + res.redirect('/login'); }; const forgot = async (req, res) => { const { email } = req.body; if (!email) { - res.status(400).json({ message: 'Email is required' }); - - return; + return res.status(400).json({ message: 'Email required' }); } const user = await userServices.findUser(email); if (!user) { - res.sendStatus(200); - - return; + return res.sendStatus(200); } - const resetToken = uuidv4(); - - user.resetToken = resetToken; - + user.resetToken = uuidv4(); await user.save(); - await emailServices.sendResetPasswordEmail(email, resetToken); - res.send({ message: 'Password reset email sent' }); + await emailServices.sendResetPasswordEmail(email, user.resetToken); + res.json({ message: 'Email sent' }); }; const resetPassword = async (req, res) => { @@ -194,32 +111,26 @@ const resetPassword = async (req, res) => { const { password, confirmation } = req.body; if (!password || password !== confirmation) { - res.status(400).json({ message: 'Passwords do not match' }); - - return; + return res.status(400).json({ message: 'Passwords do not match' }); } const user = await User.findOne({ where: { resetToken } }); if (!user) { - res.status(400).json({ message: 'Invalid reset token' }); - - return; + return res.status(400).json({ message: 'Invalid token' }); } user.password = bcrypt.hashSync(password, 10); user.resetToken = null; - await user.save(); - res.send({ message: 'Password successfully changed' }); + res.json({ message: 'Password changed' }); }; export const authController = { registerUser, activateUser, loginUser, - refresh, logout, forgot, resetPassword, diff --git a/src/controller/user.controller.js b/src/controller/user.controller.js index 1c8a541c..e340dff2 100644 --- a/src/controller/user.controller.js +++ b/src/controller/user.controller.js @@ -1,107 +1,103 @@ -import { User } from '../models/User.model.js'; import { userServices } from '../services/user.services.js'; import { emailServices } from '../services/email.services.js'; import bcrypt from 'bcrypt'; -const getAllUsers = async (req, res) => { - const users = await User.findAll(); - - res.send(users); -}; - -const getUserById = async (req, res) => { - const { userId } = req.params; - - const user = await userServices.findUserById(userId); - - res.send(user); -}; - const updateName = async (req, res) => { try { const userId = req.user.userId; const { name } = req.body; if (!name || !name.trim()) { - res.status(400).json({ message: 'Name is required' }); - - return; + return res.status(400).json({ message: 'Name is required' }); } const user = await userServices.updateNameService(userId, name); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + res.send(user); } catch (error) { - res.status(500).send(error); + res.status(500).json({ message: 'Internal server error' }); } }; const updatePassword = async (req, res) => { - const { oldPassword, newPassword, confirmation } = req.body; - const userId = req.user.userId; + try { + const { oldPassword, newPassword, confirmation } = req.body; + const userId = req.user.userId; - if (newPassword !== confirmation) { - res.status(400).json({ message: 'Passwords do not match' }); + if (!oldPassword || !newPassword || !confirmation) { + return res.status(400).json({ message: 'All fields are required' }); + } - return; - } + if (newPassword !== confirmation) { + return res.status(400).json({ message: 'Passwords do not match' }); + } - const user = await userServices.findUserById(userId); - const isValid = await bcrypt.compare(oldPassword, user.password); + const user = await userServices.findUserById(userId); - if (!isValid) { - res.status(401).json({ message: 'Old password is incorrect' }); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } - return; - } + const isValid = await bcrypt.compare(oldPassword, user.password); - user.password = bcrypt.hashSync(newPassword, 10); - await user.save(); + if (!isValid) { + return res.status(401).json({ message: 'Old password is incorrect' }); + } + + user.password = bcrypt.hashSync(newPassword, 10); + await user.save(); - res.send({ message: 'Password updated successfully' }); + res.send({ message: 'Password updated successfully' }); + } catch (error) { + res.status(500).json({ message: 'Internal server error' }); + } }; const updateEmail = async (req, res) => { - const { password, newEmail, confirmation } = req.body; - const userId = req.user.userId; - - if (!newEmail || !confirmation) { - res - .status(400) - .json({ message: 'New email and confirmation are required' }); - - return; - } + try { + const { password, newEmail, confirmation } = req.body; + const userId = req.user.userId; - if (newEmail !== confirmation) { - res.status(400).json({ message: 'Emails do not match' }); + if (!password || !newEmail || !confirmation) { + return res + .status(400) + .json({ message: 'Password, new email and confirmation are required' }); + } - return; - } + if (newEmail !== confirmation) { + return res.status(400).json({ message: 'Emails do not match' }); + } - const user = await userServices.findUserById(userId); + const user = await userServices.findUserById(userId); - const isValidPassword = await bcrypt.compare(password, user.password); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } - if (!isValidPassword) { - res.status(401).json({ message: 'Invalid password' }); + const isValidPassword = await bcrypt.compare(password, user.password); - return; - } + if (!isValidPassword) { + return res.status(401).json({ message: 'Invalid password' }); + } - const oldEmail = user.email; + const oldEmail = user.email; - user.email = newEmail; + user.email = newEmail; - await user.save(); - await emailServices.sendEmailChangedNotification(oldEmail, newEmail); + await user.save(); + await emailServices.sendEmailChangedNotification(oldEmail, newEmail); - res.send({ message: 'Email updated successfully' }); + res.send({ message: 'Email updated successfully' }); + } catch (error) { + res.status(500).json({ message: 'Internal server error' }); + } }; export const userController = { - getAllUsers, - getUserById, updateName, updateEmail, updatePassword, diff --git a/src/middlewares/guest.middleware.js b/src/middlewares/guest.middleware.js new file mode 100644 index 00000000..8b6993af --- /dev/null +++ b/src/middlewares/guest.middleware.js @@ -0,0 +1,18 @@ +import { jwtService } from '../services/jwt.services.js'; + +export const guestMiddleware = (req, res, next) => { + const auth = req.headers.authorization; + + if (!auth) { + return next(); + } + + const [, token] = auth.split(' '); + const user = jwtService.verify(token); + + if (user) { + return res.redirect('/profile'); + } + + next(); +}; diff --git a/src/router/auth.router.js b/src/router/auth.router.js index f5e09e8c..c60dee7a 100644 --- a/src/router/auth.router.js +++ b/src/router/auth.router.js @@ -1,13 +1,26 @@ import express from 'express'; import { authController } from '../controller/auth.controller.js'; +import { authMiddleware } from '../middlewares/auth.middleware.js'; +import { guestMiddleware } from '../middlewares/guest.middleware.js'; const router = express.Router(); -router.post('/auth', authController.registerUser); -router.get('/activate/:activationToken', authController.activateUser); -router.post('/login', authController.loginUser); -router.get('/logout', authController.logout); -router.post('/forgot', authController.forgot); -router.post('/password-reset/:resetToken', authController.resetPassword); +router.post('/auth', guestMiddleware, authController.registerUser); + +router.get( + '/activate/:activationToken', + guestMiddleware, + authController.activateUser, +); +router.post('/login', guestMiddleware, authController.loginUser); +router.post('/forgot', guestMiddleware, authController.forgot); + +router.post( + '/password-reset/:resetToken', + guestMiddleware, + authController.resetPassword, +); + +router.get('/logout', authMiddleware, authController.logout); export default router; diff --git a/src/services/user.services.js b/src/services/user.services.js index 89c5e199..2b7d27d1 100644 --- a/src/services/user.services.js +++ b/src/services/user.services.js @@ -1,31 +1,42 @@ import { User } from '../models/User.model.js'; -const registerUser = (name, email, password, activationToken) => - User.create({ +const registerUser = (name, email, password, activationToken) => { + return User.create({ name, email, password, activationToken, }); +}; -const findUser = (email) => User.findOne({ where: { email } }); +const findUser = (email) => { + return User.findOne({ where: { email } }); +}; -const findUserById = (id) => User.findByPk(id); +const findUserById = (id) => { + return User.findByPk(id); +}; const updateNameService = async (id, name) => { const user = await findUserById(id); + if (!user) { + return null; + } + user.name = name; await user.save(); return user; }; -const normilizeUser = (user) => ({ - userId: user.userId, - email: user.email, - name: user.name, -}); +const normilizeUser = (user) => { + return { + userId: user.userId, + email: user.email, + name: user.name, + }; +}; export const userServices = { registerUser, From a20bb17bd474928141fe927671715d944181f7f3 Mon Sep 17 00:00:00 2001 From: Gianluca Marchese Date: Fri, 6 Feb 2026 00:06:16 +0000 Subject: [PATCH 3/3] Fix login redirect to profile --- src/controller/auth.controller.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/controller/auth.controller.js b/src/controller/auth.controller.js index 34705cf5..2e076add 100644 --- a/src/controller/auth.controller.js +++ b/src/controller/auth.controller.js @@ -76,10 +76,14 @@ const loginUser = async (req, res) => { return res.status(401).json({ message: 'Invalid credentials' }); } + // token generato SOLO per sessione client, non restituito const normalized = userServices.normilizeUser(user); const token = jwtService.sign(normalized); - res.json({ accessToken: token, user: normalized }); + // opzionale: se i test non lo richiedono, puoi anche ometterlo + res.setHeader('Authorization', `Bearer ${token}`); + + return res.redirect('/profile'); }; const logout = (req, res) => {