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
279 changes: 279 additions & 0 deletions src/controllers/authController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
import { pool } from '../db.js';
import jwt from 'jsonwebtoken';
import { sendActivationMail } from '../utils/mailService.js';

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

try {
const userCheck = await pool.query('SELECT * FROM users WHERE email = $1', [
email,
]);

if (userCheck.rows.length > 0) {
return res
.status(400)
.json({ message: 'User with this email already exists.' });
}

const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/;

if (!passwordRegex.test(password)) {
return res.status(400).json({
message:
'Password must be at least 8 characters' +
' long and contain at least one number.',
});
}

const passwordHash = await bcrypt.hash(password, 10);

const activationToken = uuidv4();

const query = `
INSERT INTO users (name, email, password_hash, is_activated, activation_token)
VALUES ($1, $2, $3, $4, $5)
RETURNING id;
`;
const values = [name, email, passwordHash, false, activationToken];

await pool.query(query, values);

res.status(201).json({
message:
'Registration successful! ' +
'Please check your email to activate your account.',
});
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 sending an activation email after a user registers. While the user and activation token are created correctly, the call to sendActivationMail is missing here. You'll need to construct the activation link and send it to the user's email before sending the success response.

} catch (error) {
res.status(500).json({ message: 'Server error during registration.' });
}
};

export const activate = async (req, res) => {
const { token } = req.params;

try {
const user = await pool.query(
'SELECT * FROM users WHERE activation_token = $1',
[token],
);

if (user.rows.length === 0) {
return res
.status(400)
.json({ message: 'Invalid or expired activation link.' });
}

await pool.query(
'UPDATE users SET is_activated = true,' +
' activation_token = null WHERE activation_token = $1',
[token],
);

res
.status(200)
.json({ message: 'Account activated successfully! You can now log in.' });
} catch (error) {
res.status(500).json({ message: 'Internal server error.' });
}
};

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

try {
const result = await pool.query('SELECT * FROM users WHERE email = $1', [
email,
]);
const user = result.rows[0];

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

if (!user.is_activated) {
return res
.status(403)
.json({ message: 'Please activate your email before logging in.' });
}

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

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

const accessToken = jwt.sign(
{ id: user.id, email: user.email },
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '30m' },
);

res.status(200).json({
message: 'Login successful!',
accessToken,
user: {
id: user.id,
name: user.name,
email: user.email,
},
});
} catch (error) {
res.status(500).json({ message: 'Internal server error.' });
}
};

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

try {
const result = await pool.query('SELECT * FROM users WHERE email = $1', [
email,
]);
const user = result.rows[0];

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

const resetToken = uuidv4();

await pool.query(
'UPDATE users SET activation_token = $1 WHERE email = $2',
[resetToken, email],
);

const resetLink = `http://localhost:5173/reset-password/${resetToken}`;

await sendActivationMail(email, resetLink);

res.json({ message: 'Password reset link sent to your email.' });
} catch (error) {
res.status(500).json({ message: 'Internal server error.' });
}
};

export const resetPassword = async (req, res) => {
const { token, newPassword, confirmation } = req.body;

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

try {
const user = await pool.query(
'SELECT * FROM users WHERE activation_token = $1',
[token],
);

if (user.rows.length === 0) {
return res
.status(400)
.json({ message: 'Invalid or expired reset token.' });
}

const hashedPath = await bcrypt.hash(newPassword, 10);

await pool.query(
'UPDATE users SET password_hash = $1, ' +
'activation_token = null WHERE activation_token = $2',
[hashedPath, token],
);

res.json({
message: 'Password has been reset successfully! You can now log in.',
});
} catch (error) {
res.status(500).json({ message: 'Internal server error.' });
}
};

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

try {
await pool.query('UPDATE users SET name = $1 WHERE id = $2', [
name,
userId,
]);
res.json({ message: 'Name updated successfully!' });
} catch (error) {
res.status(500).json({ message: 'Internal server error.' });
}
};

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

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

try {
const result = await pool.query(
'SELECT password_hash FROM users WHERE id = $1',
[userId],
);
const user = result.rows[0];

const isMatch = await bcrypt.compare(oldPassword, user.password_hash);

if (!isMatch) {
return res.status(401).json({ message: 'Old password is incorrect.' });
}

const hashedPath = await bcrypt.hash(newPassword, 10);

await pool.query('UPDATE users SET password_hash = $1 WHERE id = $2', [
hashedPath,
userId,
]);

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

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

try {
const result = await pool.query(
'SELECT email, password_hash FROM users WHERE id = $1',
[userId],
);
const user = result.rows[0];

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

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

const activationToken = uuidv4();

await pool.query(
'UPDATE users SET email = $1, ' +
'is_activated = false, activation_token = $2 WHERE id = $3',
[newEmail, activationToken, userId],
);

const link = `http://localhost:3000/api/activate/${activationToken}`;

await sendActivationMail(newEmail, link);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

According to the requirements, when a user changes their email, a notification must be sent to their old email address. This is an important security measure. Currently, an activation email is sent to the new address, but there's no notification sent to the old one (user.email), which was fetched from the database earlier.


res.json({
message: 'Email updated! Please check your new email to activate it.',
});
} catch (error) {
res.status(500).json({ message: 'Internal server error.' });
}
};
8 changes: 8 additions & 0 deletions src/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Pool } from 'pg';
import dotenv from 'dotenv';

dotenv.config();

export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
22 changes: 21 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
'use strict';
import express from 'express';
import dotenv from 'dotenv';
import authRoutes from './routes/authRoutes';

dotenv.config();

const app = express();

app.use(express.json());

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

app.use((req, res) => {
res
.status(404)
.json({ message: 'Opps! This page was not found on the backend' });
});

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {});
20 changes: 20 additions & 0 deletions src/middlewars/authMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import jwt from 'jsonwebtoken';

export const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
const token = authHeader && authHeader.split(' ')[1];

if (!token) {
return res.status(401).json({ message: 'Unauthorized. Please log in.' });
}

try {
const userData = jwt.verify(token, process.env.JWT_ACCESS_SECRET);

req.user = userData;

next();
} catch (error) {
return res.status(401).json({ message: 'Invalid or expired token.' });
}
};
49 changes: 49 additions & 0 deletions src/routes/authRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Router } from 'express';
import {
activate,
forgotPassword,
login,
register,
resetPassword,
updateEmail,
updateName,
updatePassword,
} from '../controllers/authController';
import { authMiddleware } from '../middlewars/authMiddleware';

const router = Router();

router.post('/register', register);

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

router.post('/login', login);

router.post('/forgot-password', forgotPassword);

router.post('/reset-password', resetPassword);

router.post('/logout', authMiddleware, (req, res) => {
res.json({ message: 'Logout successful. Redirecting to login...' });
});

router.patch('/update-name', authMiddleware, updateName);

router.patch('/update-password', authMiddleware, updatePassword);

router.patch('/update-email', authMiddleware, updateEmail);

router.get('/test', (req, res) => {
res.json({ message: 'API is working!' });
});

router.get('/profile', authMiddleware, (req, res) => {
const user = req.user;

res.json({
message: 'Welcome to your profile!',
user,
});
});

export default router;
Loading
Loading