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
206 changes: 206 additions & 0 deletions src/controllers/auth.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { ApiError } from '../exceptions/api.error.js';
import { User } from '../models/user.js';
import { jwtService } from '../services/jwt.service.js';
import { tokenService } from '../services/token.service.js';
import { userService } from '../services/user.service.js';
import bcrypt from 'bcrypt';

function validateEmail(email) {
if (!email) {
return 'Email is required';
}

const emailPattern = /^[\w.+-]+@([\w-]+\.){1,3}[\w-]{2,}$/;

if (!emailPattern.test(email)) {
return 'Email is not valid';
}
}

function validatePassword(password) {
if (!password) {
return 'Password is required';
}

if (password.length < 6) {
return 'At least 6 characters';
}

const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/;

if (!passwordPattern.test(password)) {
return (
'Password must contain minimum 8 characters, 1 uppercase letter, ' +
'1 lowercase letter and one number'
);
}
}

async function register(request, response, next) {
const { name, email, password } = request.body;

if (!name) {
throw ApiError.badRequest('Name is required');
}

const errors = {
email: validateEmail(email),
password: validatePassword(password),
};

if (errors.email || errors.password) {
throw ApiError.badRequest('Bad request', errors);
}

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

await userService.register(name, email, hashedPassword);

response.send({ message: 'OK' });
}

async function activate(request, response) {
const { activationToken } = request.params;
const user = await User.findOne({ where: { activationToken } });

if (!user) {
response.sendStatus(404);

return;
}

user.activationToken = null;
await user.save();

response.send(userService.normalize(user));
}

async function login(request, response) {
const { email, password } = request.body;

const user = await userService.findByEmail(email);

if (!user) {
throw ApiError.unauthorized('Invalid email or password');
}

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

if (!isPasswordValid) {
throw ApiError.unauthorized('Invalid email or password');
}

if (user.activationToken) {
throw ApiError.forbidden('Please activate your email before logging in');
}

await generateTokens(response, user);
}

async function refresh(request, response) {
const { refreshToken } = request.cookies;

const userData = await jwtService.verifyRefresh(refreshToken);
const token = await tokenService.getByToken(refreshToken);

if (!userData || !token) {
throw ApiError.unauthorized();
}

const user = await userService.findByEmail(userData.email);

await generateTokens(response, user);
}

async function generateTokens(response, user) {
const normalizedUser = userService.normalize(user);

const accessToken = jwtService.sign(normalizedUser);
const refreshToken = jwtService.signRefresh(normalizedUser);

await tokenService.save(normalizedUser.id, refreshToken);

response.cookie('refreshToken', refreshToken, {
maxAge: 30 * 24 * 60 * 60 * 1000,
HttpOnly: true,
});

response.send({
user: normalizedUser,
accessToken,
});
}

async function logout(request, response) {
const { refreshToken } = request.cookies;
const userData = await jwtService.verifyRefresh(refreshToken);

if (!userData || !refreshToken) {
throw ApiError.unauthorized();
}

await tokenService.remove(userData.id);

response.sendStatus(204);
}

async function resetPasswordRequest(request, response) {
const { email } = request.body;

if (!email) {
throw ApiError.badRequest('Email is required');
}

await userService.resetPasswordRequest(email);

response.send({
message: 'Check your email for reset password instructions',
});
}

async function resetPassword(request, response) {
const { resetToken } = request.params;
const { password, confirmation } = request.body;

if (!password || !confirmation) {
throw ApiError.badRequest('Password and confirmation are required');
}

if (password !== confirmation) {
throw ApiError.badRequest('Passwords do not match', {
confirmation: 'Passwords do not match',
});
}

const passwordError = validatePassword(password);

if (passwordError) {
throw ApiError.badRequest('Invalid password', { password: passwordError });
}

await userService.resetPassword(resetToken, password);

response.send({ message: 'Password reset successfully' });
}

async function confirmNewEmail(request, response) {
const { newEmailToken } = request.params;

const user = await userService.confirmEmailChange(newEmailToken);

response.send({
message: 'Email confirmed',
user: userService.normalize(user),
});
}

export const authController = {
register,
activate,
login,
refresh,
logout,
resetPasswordRequest,
resetPassword,
confirmNewEmail,
};
70 changes: 70 additions & 0 deletions src/controllers/user.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { userService } from '../services/user.service.js';
import { ApiError } from '../exceptions/api.error.js';

const getAllActivated = async (request, response) => {
const users = await userService.getAllActivated();

response.send(users.map(userService.normalize));
};

const getProfile = async (request, response) => {
const user = await userService.findById(request.user.id);

if (!user) {
throw ApiError.notFound('User not found');
}

response.send(userService.normalize(user));
};

const changeName = async (request, response) => {
const { name } = request.body;

if (!name) {
throw ApiError.badRequest('Name is required');
}

const user = await userService.updateName(request.user.id, name);

response.send(userService.normalize(user));
};

const changePassword = async (request, response) => {
const { oldPassword, password, confirmation } = request.body;

if (!oldPassword || !password || !confirmation) {
throw ApiError.badRequest(
'Old password, password and confirmation are required',
);
}

if (password !== confirmation) {
throw ApiError.badRequest('Passwords do not match', {
confirmation: 'Passwords do not match',
});
}

await userService.changePassword(request.user.id, oldPassword, password);

response.send({ message: 'Password changed successfully' });
};

const changeEmail = async (request, response) => {
const { password, newEmail } = request.body;

if (!password || !newEmail) {
throw ApiError.badRequest('Password and new email are required');
}

await userService.requestEmailChange(request.user.id, newEmail, password);

response.send({ message: 'Check your new email for confirmation' });
};

export const userController = {
getAllActivated,
getProfile,
changeName,
changePassword,
changeEmail,
};
40 changes: 40 additions & 0 deletions src/exceptions/api.error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export class ApiError extends Error {
constructor({ message, status, errors }) {
super(message);

this.status = status;
this.errors = errors;
}

static badRequest(message, errors) {
return new ApiError({
message,
errors,
status: 400,
});
}

static forbidden(message = 'forbidden', errors) {
return new ApiError({
message,
errors,
status: 403,
});
}

static unauthorized(errors) {
return new ApiError({
message: 'unauthorized user',
errors,
status: 401,
});
}

static notFound(errors) {
return new ApiError({
message: 'not found',
errors,
status: 404,
});
}
}
40 changes: 40 additions & 0 deletions src/http/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict';
import 'dotenv/config';
import express from 'express';
import { authRouter } from '../routes/auth.route.js';
import cors from 'cors';
import { userRouter } from '../routes/user.route.js';
import { errorMiddleware } from '../middlewares/errorMiddleware.js';
import cookieParser from 'cookie-parser';

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

const app = express();

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

app.use(
cors({
origin: process.env.CLIENT_HOST,
credentials: true,
}),
);

app.use(authRouter);
app.use('/users', userRouter);

app.get('/', (request, response) => {
response.send('Hello');
});

app.use((request, response) => {
response.sendStatus(404);
});

app.use(errorMiddleware);

app.listen(PORT, () => {
// eslint-disable-next-line no-console
console.log('server is running');
});
1 change: 0 additions & 1 deletion src/index.js

This file was deleted.

24 changes: 24 additions & 0 deletions src/middlewares/authMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { jwtService } from '../services/jwt.service.js';

export const authMiddleware = (request, response, next) => {
const authorization = request.headers['authorization'] || '';
const [, token] = authorization.split(' ');

if (!authorization || !token) {
response.sendStatus(401);

return;
}

const userData = jwtService.verify(token);

if (!userData) {
response.sendStatus(401);

return;
}

request.user = userData;

next();
};
Loading
Loading