Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
1,450 changes: 1,375 additions & 75 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "node_auth-app",
"version": "1.0.0",
"type": "module",
"description": "Auth app",
"main": "src/index.js",
"scripts": {
Expand All @@ -26,5 +27,19 @@
},
"mateAcademy": {
"projectType": "javascript"
},
"dependencies": {
"bcryptjs": "^3.0.3",
"cookie-parser": "^1.4.7",
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"express-session": "^1.19.0",
"express-validator": "^7.3.2",
"jsonwebtoken": "^9.0.3",
"nodemailer": "^8.0.7",
"pg": "^8.20.0",
"sequelize": "^6.37.8",
"uuid": "^14.0.0"
}
}
16 changes: 16 additions & 0 deletions src/config/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Sequelize } from 'sequelize';
import dotenv from 'dotenv';

dotenv.config();

const sequelize = new Sequelize(
process.env.DB_NAME,
process.env.DB_USER,
process.env.DB_PASSWORD,
{
host: process.env.DB_HOST,
dialect: 'postgres',
},
);

export default sequelize;
97 changes: 97 additions & 0 deletions src/controllers/user.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import userService from '../services/user.service.js';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires password reset functionality with: 1) Ask for email, 2) Show email sent page, 3) Reset password confirmation page with password and confirmation fields that must be equal, 4) Show success page with link to login. None of these endpoints exist - missing /forgot-password and /reset-password/:token routes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires the profile page to allow changing name. The updateName method is not implemented in user.service.js and there is no corresponding route in the router for updating the user's name.


class UserController {
async registration(req, res) {
try {
const { name, email, password } = req.body;
const userData = await userService.registration(name, email, password);

return res.json(userData);
} catch (e) {
res.status(400).json({ message: e.message });
}
}

async login(req, res) {
try {
const { email, password } = req.body;
const userData = await userService.login(email, password);

res.cookie('token', userData.token, {
maxAge: 24 * 60 * 60 * 1000,
httpOnly: true,
sameSite: 'lax',
});

return res.json(userData);
} catch (e) {
res.status(401).json({ message: e.message });
}
}

async activate(req, res) {
try {
const { token } = req.params;
const jwtToken = await userService.activate(token);

res.cookie('token', jwtToken, {
maxAge: 24 * 60 * 60 * 1000,
httpOnly: true,
});

return res.redirect(`${process.env.CLIENT_URL}/profile?activated=true`);
} catch (e) {
res.status(400).json({ message: e.message });
}
Comment on lines +41 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing confirmation field validation for password update - the task requires: new password AND confirmation (must equal new password). Currently only validates oldPassword and newPassword. Add: body('confirmation').equals(req.body.newPassword).withMessage('Пароли не совпадают')

}

async logout(req, res) {
try {
res.clearCookie('token');
Comment on lines +1 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing route for name update - the updateName method exists in user.service.js (lines 89-98) but there's no PUT /update-name route with authMiddleware to expose it. Add: router.put('/update-name', authMiddleware, body('name').notEmpty()..., userController.updateName)

Comment on lines +1 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing forgotPassword route - the forgotPassword method exists in user.service.js (lines 91-107) but there's no route/controller method to trigger it. This is required for Step 1 of the password reset flow (email input form).

Comment on lines +1 to +50
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing resetPassword route - the resetPassword method exists in user.service.js (lines 109-119) but there's no route to handle the token-based password reset (e.g., POST /reset-password/:token). This is required for Step 3 of the password reset flow (reset confirmation page). The route should validate that password and confirmation fields are equal before calling the service.


return res.json({ message: 'Вышли из системы' });
} catch (e) {
res.status(500).json({ message: 'Ошибка логаута' });
}
}

async getProfile(req, res) {
try {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The controller calls userService.getProfile(req.user.userId) but this method is not implemented in user.service.js. The service only exports registration, activate, login, forgotPassword, updatePassword, and updateEmail. A getProfile method is needed to fetch user data by ID.

const user = await userService.getProfile(req.user.userId);

return res.json(user);
} catch (e) {
res.status(500).json({ message: e.message });
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing confirmation field validation for email update - the task requires: password, new email, AND confirm email (confirmation must equal new email). Currently only validates password and newEmail. Add: body('confirmEmail').isEmail().withMessage('Введите подтверждение email') and validate equality in controller or service.

async updatePassword(req, res) {
try {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires password change with fields: old password, new password and confirmation. Currently only oldPassword and newPassword are sent from the frontend. The confirmation field is missing from both the controller and service.

const { oldPassword, newPassword } = req.body;

await userService.updatePassword(
req.user.userId,
oldPassword,
newPassword,
);

return res.json({ message: 'Пароль успешно обновлен' });
} catch (e) {
res.status(400).json({ message: e.message });
}
}

async updateEmail(req, res) {
try {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires email change with: type password, confirm the new email, notify old email. Currently only password and newEmail are accepted. The confirmEmail field is missing from both controller and service.

const { password, newEmail } = req.body;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

userService.forgotPassword calls emailService.sendNotification but this method does not exist in email.service.js. Only sendActivationMail is implemented. This will cause a runtime error.


await userService.updateEmail(req.user.userId, password, newEmail);

return res.json({ message: 'Инструкции отправлены на почту' });
} catch (e) {
res.status(400).json({ message: e.message });
}
}
}

export default new UserController();
Comment on lines +1 to +135
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing updateName controller method - userService.updateName exists but there's no corresponding updateName method in the controller. Add: async updateName(req, res) { await userService.updateName(req.user.userId, req.body.name); ... }

Comment on lines +1 to +135
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing forgotPassword controller method - userService.forgotPassword exists but there's no corresponding controller method. Add: async forgotPassword(req, res) { await userService.forgotPassword(req.body.email); return res.json({ message: 'Email sent' }); }

Comment on lines +1 to +135
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing resetPassword controller method - userService.resetPassword exists but there's no corresponding controller method. Add: async resetPassword(req, res) { const { token } = req.params; const { password } = req.body; await userService.resetPassword(token, password); }

40 changes: 40 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,41 @@
'use strict';

import express from 'express';
import cookieParser from 'cookie-parser';
import cors from 'cors';

import router from './router/index.js';

import sequelize from './config/db.js';

const app = express();

app.use(express.json());
app.use(cookieParser());

app.use(
cors({
credentials: true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SQL injection vulnerability. Using template literals with process.env.DB_NAME directly in SQL queries. Use parameterized queries instead.

Comment on lines +17 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SQL injection vulnerability: process.env.DB_NAME is directly interpolated into the SQL query. Use parameterized queries instead.

origin: true,
}),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SQL injection vulnerability. Using template literals with process.env.DB_NAME directly in SQL query. Use parameterized queries instead.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SQL injection vulnerability: process.env.DB_NAME is directly interpolated into the SQL query. Use parameterized queries instead.

app.use('/api', router);

app.use((req, res) => {
res.status(404).json({ message: 'Page Not Found' });
});

const start = async () => {
try {
await sequelize.authenticate();
await sequelize.sync();
// eslint-disable-next-line no-console
app.listen(process.env.PORT || 5000, () => console.log('Server started'));
} catch (e) {
// eslint-disable-next-line no-console
console.log(e);
}
};

start();
36 changes: 36 additions & 0 deletions src/init_db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import pg from 'pg';
import dotenv from 'dotenv';

dotenv.config();

const createDatabase = async () => {
const client = new pg.Client({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: 'postgres',
});

try {
await client.connect();

const res = await client.query(
`SELECT 1 FROM pg_database WHERE datname = '${process.env.DB_NAME}'`,
);

if (res.rowCount === 0) {
await client.query(`CREATE DATABASE ${process.env.DB_NAME}`);
/* eslint-disable no-console */
console.log(` База данных ${process.env.DB_NAME} создана!`);
} else {
console.log('ℹБаза данных уже существует.');
}
} catch (err) {
console.error('Ошибка при создании базы:', err);
} finally {
await client.end();
}
};

createDatabase();
22 changes: 22 additions & 0 deletions src/middleware/auth.middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import tokenService from '../services/token.service.js';

export default function (req, res, next) {
try {
const token = req.cookies.token;

if (!token) {
return res.status(401).json({ message: 'Пользователь не авторизован' });
}

const userData = tokenService.validateToken(token);

if (!userData) {
return res.status(401).json({ message: 'Невалидный токен' });
}

req.user = userData;
next();
} catch (e) {
return res.status(401).json({ message: 'Пользователь не авторизован' });
}
}
Comment on lines +18 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SQL injection vulnerability - Using template literals with process.env.DB_NAME directly in SQL queries. Use parameterized queries instead: client.query('SELECT 1 FROM pg_database WHERE datname = $1', [process.env.DB_NAME])

13 changes: 13 additions & 0 deletions src/middleware/validation.middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { validationResult } from 'express-validator';

export default function (req, res, next) {
const errors = validationResult(req);

if (!errors.isEmpty()) {
return res.status(400).json({
message: 'Ошибка при валидации',
errors: errors.array(),
});
}
next();
}
19 changes: 19 additions & 0 deletions src/models/user.model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { DataTypes } from 'sequelize';
import sequelize from '../config/db.js';

const User = sequelize.define(
'User',
{
name: { type: DataTypes.STRING, allowNull: false },
email: { type: DataTypes.STRING, unique: true, allowNull: false },
password: { type: DataTypes.STRING, allowNull: false },
isActive: { type: DataTypes.BOOLEAN, defaultValue: false },
activationToken: { type: DataTypes.STRING },
resetToken: { type: DataTypes.STRING },

pendingEmail: { type: DataTypes.STRING },
},
{ timestamps: true },
);

export default User;
57 changes: 57 additions & 0 deletions src/router/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Router } from 'express';
import { body } from 'express-validator';
import userController from '../controllers/user.controller.js';
import authMiddleware from '../middleware/auth.middleware.js';
import validationMiddleware from '../middleware/validation.middleware.js';

const router = new Router();

router.post(
'/registration',
body('email').isEmail().withMessage('Некорректный email'),
body('password')
.isLength({ min: 8 })
.withMessage('Пароль должен быть не менее 8 символов')
.matches(/\d/)
.withMessage('Пароль должен содержать хотя бы одну цифру')
.matches(/[A-Z]/)
.withMessage('Пароль должен содержать заглавную букву'),
body('name').notEmpty().withMessage('Имя не может быть пустым'),
validationMiddleware,
userController.registration,
);
Comment on lines +19 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SQL injection vulnerability - process.env.DB_NAME is directly interpolated into SQL queries. Use parameterized queries: SELECT 1 FROM pg_database WHERE datname = $1 with [process.env.DB_NAME].


router.get('/activate/:token', userController.activate);

router.post(
'/login',
body('email').isEmail().withMessage('Некорректный email'),
body('password').notEmpty().withMessage('Введите пароль'),
validationMiddleware,
userController.login,
);

router.get('/profile', authMiddleware, userController.getProfile);
router.post('/logout', authMiddleware, userController.logout);

router.put(
'/update-password',
authMiddleware,
body('oldPassword').notEmpty().withMessage('Введите старый пароль'),
body('newPassword')
.isLength({ min: 8 })
.withMessage('Новый пароль от 8 символов'),
validationMiddleware,
userController.updatePassword,
Comment on lines +70 to +86
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires password change with 'old password, new password and confirmation'. The route only accepts oldPassword and newPassword. Add validation for a confirmation field that must equal newPassword.

Comment on lines +70 to +86
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing confirmation field validation for password update. The task requires: 'require an old one, new password and confirmation'. Add body('confirmation') validator that checks it equals newPassword.

);

router.put(
'/update-email',
authMiddleware,
body('newEmail').isEmail().withMessage('Введите корректный новый email'),
body('password').notEmpty().withMessage('Для подтверждения нужен пароль'),
validationMiddleware,
userController.updateEmail,
Comment on lines +88 to +102
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires email change with 'type password, confirm the new email, notify old email about the change'. The route only accepts password and newEmail. Add validation for a confirmEmail field that must equal newEmail.

Comment on lines +1 to +102
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing PUT /update-name route - needed for profile name change. Add with authMiddleware and name validation.

Comment on lines +1 to +102
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing POST /forgot-password route - Step 1 of password reset flow requires an endpoint to initiate password reset by email.

Comment on lines +1 to +102
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing POST /reset-password/:token route - Step 3 of password reset flow requires an endpoint to actually reset the password with the token.

Comment on lines +88 to +102
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing confirmEmail field validation for email update. The task requires: 'type the password, confirm the new email'. Add body('confirmEmail').isEmail() and validate it equals newEmail.

);

Comment on lines +32 to +104
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires profile page to allow changing name ('You can change a name'), but there's no /update-name route and no corresponding controller method. Add a PUT route for updating user name.

Comment on lines +32 to +104
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The task requires password reset functionality: 'Ask for an email' → 'Show email sent page' → 'Reset Password confirmation page (with password and confirmation fields that must be equal)' → 'Show Success page with link to login'. None of these endpoints exist - missing /forgot-password and /reset-password/:token routes.

export default router;
36 changes: 36 additions & 0 deletions src/services/email.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import nodemailer from 'nodemailer';

class MailService {
constructor() {
this.transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: false,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD,
},
});
}

async sendActivationMail(to, link) {
await this.transporter.sendMail({
from: process.env.SMTP_USER,
to,
subject: 'Активация аккаунта',
text: '',
html: `<div><h1>Для активации перейдите по ссылке</h1><a href="${link}">${link}</a></div>`,
Comment on lines +18 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SQL injection vulnerability: Using template literals with process.env.DB_NAME directly in SQL queries (lines 18, 22). Use parameterized queries instead: client.query('SELECT 1 FROM pg_database WHERE datname = $1', [process.env.DB_NAME]).

});
}

async sendNotification(to, subject, text) {
await this.transporter.sendMail({
from: process.env.SMTP_USER,
to,
subject,
text,
});
}
}

export default new MailService();
Comment on lines +1 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The sendNotification method is called in user.service.js (lines 86-89 and 119-122) but is not defined in email.service.js. This will cause runtime errors when users request password reset or change their email.

17 changes: 17 additions & 0 deletions src/services/token.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import jwt from 'jsonwebtoken';

class TokenService {
generateToken(payload) {
return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: '24h' });
}

validateToken(token) {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (e) {
return null;
}
}
}

export default new TokenService();
Loading
Loading