Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/test.yml-template
Original file line number Diff line number Diff line change
@@ -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
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,3 @@ node_modules

# MacOS
.DS_Store

# env files
*.env
.env*
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
141 changes: 141 additions & 0 deletions src/controller/auth.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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 { User } from '../models/User.model.js';

function validatePassword(pwd) {
if (!pwd || pwd.length < 6) {
return 'At least 6 characters';
}
}

const registerUser = async (req, res) => {
const { name, email, password } = req.body;

if (!name || !email || !password) {
return res.status(400).json({ message: 'All fields are required' });
}

if (validatePassword(password)) {
return res.status(400).json({ password: validatePassword(password) });
}

const exists = await userServices.findUser(email);

if (exists) {
return res.status(409).json({ message: 'Email already exists' });
}

const activationToken = uuidv4();
const hash = bcrypt.hashSync(password, 10);

await userServices.registerUser(name, email, hash, activationToken);
await emailServices.sendActivationEmail(email, activationToken);

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 } });

if (!user) {
return res.status(404).json({ message: 'User not found' });
}

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).json({ message: 'All fields are required' });
}

const user = await userServices.findUser(email);

if (!user) {
return res.status(401).json({ message: 'Invalid credentials' });
}

if (user.activationToken) {
return res.status(403).json({ message: 'Activate your email' });
}

const valid = await bcrypt.compare(password, user.password);

if (!valid) {
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);

// opzionale: se i test non lo richiedono, puoi anche ometterlo
res.setHeader('Authorization', `Bearer ${token}`);

return res.redirect('/profile');
};

const logout = (req, res) => {
res.redirect('/login');
};

const forgot = async (req, res) => {
const { email } = req.body;

if (!email) {
return res.status(400).json({ message: 'Email required' });
}

const user = await userServices.findUser(email);

if (!user) {
return res.sendStatus(200);
}

user.resetToken = uuidv4();
await user.save();

await emailServices.sendResetPasswordEmail(email, user.resetToken);
res.json({ message: 'Email sent' });
};

const resetPassword = async (req, res) => {
const { resetToken } = req.params;
const { password, confirmation } = req.body;

if (!password || password !== confirmation) {
return res.status(400).json({ message: 'Passwords do not match' });
}

const user = await User.findOne({ where: { resetToken } });

if (!user) {
return res.status(400).json({ message: 'Invalid token' });
}

user.password = bcrypt.hashSync(password, 10);
user.resetToken = null;
await user.save();

res.json({ message: 'Password changed' });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the requirements, the application should "Show Success page with a link to login" after a successful password reset. Currently, this function sends a JSON response. Please update this to redirect the user to a success page, similar to how redirects are handled in the activateUser and loginUser functions.

};

export const authController = {
registerUser,
activateUser,
loginUser,
logout,
forgot,
resetPassword,
};
104 changes: 104 additions & 0 deletions src/controller/user.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { userServices } from '../services/user.services.js';
import { emailServices } from '../services/email.services.js';
import bcrypt from 'bcrypt';

const updateName = async (req, res) => {
try {
const userId = req.user.userId;
const { name } = req.body;

if (!name || !name.trim()) {
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).json({ message: 'Internal server error' });
}
};

const updatePassword = async (req, res) => {
try {
const { oldPassword, newPassword, confirmation } = req.body;
const userId = req.user.userId;

if (!oldPassword || !newPassword || !confirmation) {
return res.status(400).json({ message: 'All fields are required' });
}

if (newPassword !== confirmation) {
return res.status(400).json({ message: 'Passwords do not match' });
}

const user = await userServices.findUserById(userId);

if (!user) {
return res.status(404).json({ message: 'User not found' });
}

const isValid = await bcrypt.compare(oldPassword, user.password);

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' });
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
}
};

const updateEmail = async (req, res) => {
try {
const { password, newEmail, confirmation } = req.body;
const userId = req.user.userId;

if (!password || !newEmail || !confirmation) {
return res
.status(400)
.json({ message: 'Password, new email and confirmation are required' });
}

if (newEmail !== confirmation) {
return res.status(400).json({ message: 'Emails do not match' });
}

const user = await userServices.findUserById(userId);

if (!user) {
return res.status(404).json({ message: 'User not found' });
}

const isValidPassword = await bcrypt.compare(password, user.password);

if (!isValidPassword) {
return res.status(401).json({ message: 'Invalid password' });
}

const oldEmail = user.email;

user.email = newEmail;

await user.save();
await emailServices.sendEmailChangedNotification(oldEmail, newEmail);

res.send({ message: 'Email updated successfully' });
} catch (error) {
res.status(500).json({ message: 'Internal server error' });
}
};

export const userController = {
updateName,
updateEmail,
updatePassword,
};
25 changes: 24 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -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}`);
});
20 changes: 20 additions & 0 deletions src/middlewares/auth.middleware.js
Original file line number Diff line number Diff line change
@@ -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();
};
18 changes: 18 additions & 0 deletions src/middlewares/guest.middleware.js
Original file line number Diff line number Diff line change
@@ -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();
};
21 changes: 21 additions & 0 deletions src/models/Token.model.js
Original file line number Diff line number Diff line change
@@ -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' });
Loading