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

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

await sendMail(
email,
'Account activation',
`Please activate your account by clicking: <a href="${activationLink}">${activationLink}</a>`,
);

res
.status(201)
.json({ message: 'Registration successful! Please check your email.' });
} catch (error) {
res.status(500).json({ message: 'Internal server error.' });
}
};

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 sendMail(
email,
'Password Reset Request',
`
<div>
<h1>Password Reset</h1>
<p>To reset your password, please click the link below:</p>
<a href="${resetLink}">${resetLink}</a>
<p>If you didn't request this, just ignore this email.</p>
</div>
`,
);

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 oldEmail = user.email;

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 sendMail(
newEmail,
'Confirm your new email',
`Activate here: ${link}`,
);

await sendMail(
oldEmail,
'Your email has been changed',
`The email address for your account was recently changed to ${newEmail}. If you didn't do this, please contact support.`,
);

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.' });
}
};
Loading
Loading