Skip to content

Commit

Permalink
auth, role, user functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
SAINIAbhishek committed Nov 30, 2024
1 parent ae93c5e commit 572d28f
Show file tree
Hide file tree
Showing 25 changed files with 1,236 additions and 67 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ jobs:
path: node_modules
key: ${{ runner.os }}-auth-${{ hashFiles('package-lock.json') }}

- name: Checking Formatting with Prettier
run: npm run prettier
- name: Formatting with Prettier
run: npm run format
working-directory: ./

# Step: Security audit check
Expand Down
7 changes: 7 additions & 0 deletions src/config/regex-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Regex } from '../types/regex-type';

const regex: Regex = {
removeQuotes: /['"]+/g,
};

export default regex;
255 changes: 255 additions & 0 deletions src/controllers/auth-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import asyncHandler from 'express-async-handler';
import logger from '../middleware/logging-handler';
import {
SuccessMsgResponse,
SuccessResponse,
TokenRefreshResponse,
} from '../services/response-service';
import {
createTokens,
generateHashTokenKey,
generateTokenKey,
getAccessToken,
validatePasswordUpdate,
validateTokenData,
} from '../helpers/auth-helper';
import config from '../config';
import UserModel from '../models/user-model';
import { NextFunction, Request, Response } from 'express';
import { defaultRolePopulate } from '../helpers/role-helper';
import { AuthFailureError, BadRequestError, InternalError } from '../services/error-service';
import bcrypt from 'bcrypt';
import { Token } from '../types/token-type';
import { sanitizedUser } from '../helpers/user-helper';
import AuthService from '../services/auth-service';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { RoleModel } from '../models/role-model';
import { RoleNameEnum } from '../types/role-type';

class AuthController {
forgotPassword = asyncHandler(async (req: Request, res: Response, next: NextFunction) => {
const { email } = req.body;

const user = await UserModel.findOne({ email: email });

if (!user) {
logger.info(`Attempted password reset for non-existent email: ${email}`);

new SuccessMsgResponse(
'If the email exists, you will receive an email to reset the password.'
).send(res);
} else {
const resetToken = generateTokenKey();

user.passwordResetTokenRaw = resetToken; // Raw token for email
user.passwordResetToken = generateHashTokenKey(resetToken); // Hashed token for storage
user.passwordResetTokenExpires = new Date(
Date.now() + config.token.passwordResetTokenValidity
).toString();

await user.save({ validateBeforeSave: false });

req.user = {
id: user._id.toString() ?? '',
email: email,
firstname: user.firstname ?? '',
passwordResetTokenRaw: user.passwordResetTokenRaw,
};
}

next();
});

resetPassword = asyncHandler(async (req: Request, _res: Response, next) => {
const { password, email } = req.body;
const { token } = req.params;

let filter = {
passwordResetTokenRaw: token,
passwordResetToken: token,
email: email,
passwordResetTokenExpires: { $gt: Date.now() },
};

if (!token) {
logger.info(`Attempted password reset, ${JSON.stringify(filter)}`);
throw new BadRequestError('Token is invalid or has been expired.');
}

filter = {
...filter,
passwordResetToken: generateHashTokenKey(token),
};

const user = await UserModel.findOne(filter);

if (!user) {
logger.info(`Attempted password reset, ${JSON.stringify(filter)}`);
throw new BadRequestError('Token is invalid or has been expired.');
}

user.passwordResetToken = undefined;
user.passwordResetTokenRaw = undefined;
user.passwordResetTokenExpires = undefined;
user.passwordUpdatedAt = Date.now().toString();

user.password = await AuthService.generateHashPassword(password);

await user.save();

req.user = {
id: user._id.toString() ?? '',
email: email,
firstname: user.firstname ?? '',
};

req.resetPassword = {
isPasswordUpdated: true,
};

next();
});

isAuthorized = asyncHandler(async (req: Request, _res: Response, next: NextFunction) => {
const { authorization } = req.headers;
const token = getAccessToken(authorization);

if (!token) {
throw new AuthFailureError('Authorization token is missing');
}

const accessTokenPayload = jwt.verify(token, config.token.accessTokenSecret) as JwtPayload;
validateTokenData(accessTokenPayload, 'Unauthorized');

const userId = accessTokenPayload.sub ?? '';
const user = await UserModel.findById(userId).select('+passwordUpdatedAt').lean().exec();

if (!user) {
throw new AuthFailureError('User not found');
}

validatePasswordUpdate(accessTokenPayload, user);

req.session = {
...req.session,
accessToken: token,
userId: userId,
};

next();
});

register = asyncHandler(async (req, res) => {
const { email, password, firstname, lastname } = req.body;

const existingUser = await UserModel.findOne({ email: email }).lean().exec();
if (existingUser) {
logger.info(`Attempted user register for already registered, ${email}`);
throw new BadRequestError('User already registered');
}

const role = await RoleModel.findOne({ name: RoleNameEnum.USER }).lean().exec();
if (!role) {
throw new InternalError('Role must be defined');
}

const hashedPassword = await AuthService.generateHashPassword(password);

const userObj = {
email,
password: hashedPassword,
firstname,
lastname,
roles: [role],
};

await UserModel.create(userObj);

new SuccessMsgResponse('The user has been registered successfully').send(res);
});

login = asyncHandler(async (req, res) => {
const email = req.body.email;

const user = await UserModel.findOne({ email: email })
.select('+password')
.populate(defaultRolePopulate)
.lean()
.exec();

if (!user?.password) {
throw new BadRequestError('Your email address or password is incorrect');
}

const isMatched = await bcrypt.compare(req.body.password, user.password);

if (!isMatched) {
throw new AuthFailureError('Your credentials are incorrect');
}

const tokens: Token = createTokens(user);
res.cookie(config.cookie.login, tokens.refreshToken, {
httpOnly: true,
secure: true,
sameSite: true,
maxAge: config.cookie.maxAge,
});

new SuccessResponse('User logged in successfully', {
tokens,
user: sanitizedUser(user),
}).send(res);
});

refreshToken = asyncHandler(async (req: Request, res: Response) => {
const refreshToken = (req.cookies && req.cookies[config.cookie.login]) ?? null;

if (!refreshToken) {
throw new AuthFailureError('Refresh token is missing');
}

const refreshTokenPayload = jwt.verify(
refreshToken,
config.token.refreshTokenSecret
) as JwtPayload;

validateTokenData(refreshTokenPayload);

const userId = refreshTokenPayload.sub ?? '';
const user = await UserModel.findById(userId)
.select('+passwordUpdatedAt')
.populate(defaultRolePopulate)
.lean()
.exec();

if (!user) {
throw new AuthFailureError('User not found');
}

validatePasswordUpdate(refreshTokenPayload, user);

const tokens: Token = createTokens(user);

new TokenRefreshResponse('Token issued', {
tokens: {
accessToken: tokens.accessToken,
},
user: sanitizedUser(user),
}).send(res);
});

logout = asyncHandler(async (req, res) => {
const refreshToken = (req.cookies && req.cookies[config.cookie.login]) ?? null;

if (refreshToken) {
res.clearCookie(config.cookie.login, {
httpOnly: true,
secure: true,
sameSite: 'none',
});
}
new SuccessMsgResponse('User logged out successfully').send(res);
});
}

export default new AuthController();
113 changes: 113 additions & 0 deletions src/controllers/email-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import asyncHandler from 'express-async-handler';
import { EmailModel } from '../models/email-model';
import { BadRequestError, InternalError } from '../services/error-service';
import { Email, EmailStatusEnum } from '../types/email-type';
import { Request, Response } from 'express';
import { emailPreparePasswordUpdate, emailPrepareReset } from '../helpers/email-helper';
import EmailService from '../services/email-service';
import { User } from '../types/user-type';
import logger from '../middleware/logging-handler';
import UserModel from '../models/user-model';
import { SuccessMsgResponse, SuccessResponse } from '../services/response-service';

class EmailController {
/**
* Handles the password update process by sending a update update email to the user.
*/
passwordUpdateSuccessfully = asyncHandler(async (req: Request, res: Response) => {
const user = req.user;
const isPasswordUpdated = req.resetPassword.isPasswordUpdated;

if (!isPasswordUpdated || !user?.id) {
throw new BadRequestError(
'There was an error while resetting password. Please try again later.'
);
}

const email: Email = emailPreparePasswordUpdate(user.email, user.firstname);

try {
await EmailService.sendEmail({
to: email.to,
subject: email.subject,
html: email.content,
});
email.status = EmailStatusEnum.SENT;
} catch (err: unknown) {
email.status = EmailStatusEnum.ERROR;

email.error =
err instanceof Error
? err.message
: 'Nodemailer error while sending the password update mail.';

logger.error(err);
}

await EmailModel.create(email);

new SuccessMsgResponse('The password has been updated successfully').send(res);
});

/**
* Handles the password reset process by sending a reset email to the user.
*/
resetPassword = asyncHandler(async (req: Request, res: Response) => {
const user = req.user;

if (!user) {
throw new BadRequestError(
'There was an error while resetting password. Please try again later.'
);
}

const passwordResetTokenRaw = user.passwordResetTokenRaw;
if (!passwordResetTokenRaw) {
throw new BadRequestError(
'There was an error while resetting password. Please try again later.'
);
}

const email: Email = emailPrepareReset(user.email, user.firstname, passwordResetTokenRaw);

try {
await EmailService.sendEmail({
to: email.to,
subject: email.subject,
html: email.content,
});
email.status = EmailStatusEnum.SENT;
} catch (err: unknown) {
email.status = EmailStatusEnum.ERROR;

email.error =
err instanceof Error
? err.message
: 'Nodemailer error while sending the password reset mail.';

logger.error(err);

const updateFields: User = {
passwordResetToken: undefined,
passwordResetTokenRaw: undefined,
passwordResetTokenExpires: undefined,
};

await UserModel.findOneAndUpdate({ _id: user.id }, { $set: updateFields }).lean().exec();
}

await EmailModel.create(email);

if (email.status === EmailStatusEnum.ERROR) {
throw new InternalError(
'There was an error while sending the reset password email. Please try again later.'
);
}

new SuccessResponse('The password reset email has been sent successfully', {
passwordResetToken: user.passwordResetTokenRaw,
}).send(res);
});
}

export default new EmailController();
Loading

0 comments on commit 572d28f

Please sign in to comment.