Skip to content

Commit

Permalink
feat: add user +auth controllers
Browse files Browse the repository at this point in the history
  • Loading branch information
saisilinus committed Dec 16, 2021
1 parent 26736f7 commit cb09ae3
Show file tree
Hide file tree
Showing 13 changed files with 6,067 additions and 5,902 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@types/express-rate-limit": "^5.1.3",
"@types/morgan": "^1.9.3",
"@types/node": "^16.11.12",
"@types/nodemailer": "^6.4.4",
"@types/passport-jwt": "^3.0.6",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",
Expand Down
72 changes: 72 additions & 0 deletions src/Auth/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import httpStatus from 'http-status';
import { Request, Response } from 'express';
import catchAsync from '../utils/catchAsync';
import { createUser } from '../Users/user.service';
import { generateAuthTokens, generateResetPasswordToken, generateVerifyEmailToken } from '../Tokens/token.service';
import { loginUserWithEmailAndPassword, logout, refreshAuth, resetPassword, verifyEmail } from './auth.service';
import {
sendAccountCreated,
sendResetPasswordEmail,
sendSuccessfulRegistration,
sendVerificationEmail,
} from '../Email/email.service';
import config from '../config/config';
import { AccessAndRefreshTokens } from '../Tokens/token.interfaces';

export const sendTokens = (res: Response, tokens: AccessAndRefreshTokens) => {
res.cookie('accessToken', tokens.access, config.jwt.cookieOptions);
res.cookie('refreshToken', tokens.refresh, config.jwt.cookieOptions);
};

export const register = catchAsync(async (req: Request, res: Response) => {
const user = await createUser(req.body);
const tokens = await generateAuthTokens(user);
const verifyEmailToken = await generateVerifyEmailToken(user);
await sendSuccessfulRegistration(user.email, verifyEmailToken, user.name);
sendTokens(res, tokens);
res.status(httpStatus.CREATED).send({ user });
});

export const login = catchAsync(async (req: Request, res: Response) => {
const { email, password } = req.body;
const user = await loginUserWithEmailAndPassword(email, password);
const tokens = await generateAuthTokens(user);
sendTokens(res, tokens);
res.send({ user });
});

export const logoutController = catchAsync(async (req: Request, res: Response) => {
await logout(req.cookies.refreshToken);
res.status(httpStatus.NO_CONTENT).send();
});

export const refreshTokens = catchAsync(async (req: Request, res: Response) => {
const tokens = await refreshAuth(req.cookies.refreshToken);
sendTokens(res, tokens);
res.status(httpStatus.OK).send();
});

export const forgotPassword = catchAsync(async (req: Request, res: Response) => {
const resetPasswordToken = await generateResetPasswordToken(req.body.email);
await sendResetPasswordEmail(req.body.email, resetPasswordToken);
res.status(httpStatus.NO_CONTENT).send();
});

export const resetPasswordController = catchAsync(async (req: Request, res: Response) => {
await resetPassword(req.cookies.resetPasswordToken, req.body.password);
res.status(httpStatus.NO_CONTENT).send();
});

export const sendVerificationEmailController = catchAsync(async (req: Request, res: Response) => {
const verifyEmailToken = await generateVerifyEmailToken(req.user);
await sendVerificationEmail(req.user.email, verifyEmailToken, req.user.name);
res.status(httpStatus.NO_CONTENT).send();
});

export const verifyEmailController = catchAsync(async (req: Request, res: Response) => {
const user = await verifyEmail(req.cookies.verifyEmailToken);
if (user) {
await sendAccountCreated(user.email, user.name);
}
res.status(httpStatus.NO_CONTENT).send();
});
7 changes: 4 additions & 3 deletions src/Auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,18 @@ export const resetPassword = async (resetPasswordToken: string, newPassword: str
/**
* Verify email
* @param {string} verifyEmailToken
* @returns {Promise<void>}
* @returns {Promise<IUserDoc | null>}
*/
export const verifyEmail = async (verifyEmailToken: string): Promise<void> => {
export const verifyEmail = async (verifyEmailToken: string): Promise<IUserDoc | null> => {
try {
const verifyEmailTokenDoc = await verifyToken(verifyEmailToken, tokenTypes.VERIFY_EMAIL);
const user = await getUserById(verifyEmailTokenDoc.user);
if (!user) {
throw new Error();
}
await Token.deleteMany({ user: user.id, type: tokenTypes.VERIFY_EMAIL });
await updateUserById(user.id, { isEmailVerified: true });
const updatedUser = await updateUserById(user.id, { isEmailVerified: true });
return updatedUser;
} catch (error) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Email verification failed');
}
Expand Down
7 changes: 7 additions & 0 deletions src/Email/email.interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Message {
from: string;
to: string;
subject: string;
text: string;
html?: string;
}
118 changes: 118 additions & 0 deletions src/Email/email.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import nodemailer from 'nodemailer';
import config from '../config/config';
import logger from '../config/logger';
import { Message } from './email.interfaces';

export const transport = nodemailer.createTransport(config.email.smtp);
/* istanbul ignore next */
if (config.env !== 'test') {
transport
.verify()
.then(() => logger.info('Connected to email server'))
.catch(() => logger.warn('Unable to connect to email server. Make sure you have configured the SMTP options in .env'));
}

/**
* Send an email
* @param {string} to
* @param {string} subject
* @param {string} text
* @param {string} html
* @returns {Promise<void>}
*/
export const sendEmail = async (to: string, subject: string, text: string, html: string): Promise<void> => {
const msg: Message = { from: config.email.from, to, subject, text, html };
await transport.sendMail(msg);
};

/**
* Send reset password email
* @param {string} to
* @param {string} token
* @returns {Promise<void>}
*/
export const sendResetPasswordEmail = async (to: string, token: string): Promise<void> => {
const subject = 'Reset password';
// replace this url with the link to the reset password page of your front-end app
const resetPasswordUrl = `http://link-to-app/reset-password?token=${token}`;
const text = `Hi,
To reset your password, click on this link: ${resetPasswordUrl}
If you did not request any password resets, then ignore this email.`;
const html = `<div style="margin:30px; padding:30px; border:1px solid black; border-radius: 20px 10px;"><h4><strong>Dear user,</strong></h4>
<p>To reset your password, click on this link: ${resetPasswordUrl}</p>
<p>If you did not request any password resets, please ignore this email.</p>
<p>Thanks,</p>
<p><strong>Team</strong></p></div>`;
await sendEmail(to, subject, text, html);
};

/**
* Send verification email
* @param {string} to
* @param {string} token
* @param {string} name
* @returns {Promise<void>}
*/
export const sendVerificationEmail = async (to: string, token: string, name: string): Promise<void> => {
const subject = 'Email Verification';
// replace this url with the link to the email verification page of your front-end app
const verificationEmailUrl = `http://link-to-app/verify-email?token=${token}`;
const text = `Hi ${name},
To verify your email, click on this link: ${verificationEmailUrl}
If you did not create an account, then ignore this email.`;
const html = `<div style="margin:30px; padding:30px; border:1px solid black; border-radius: 20px 10px;"><h4><strong>Hi ${name},</strong></h4>
<p>To verify your email, click on this link: ${verificationEmailUrl}</p>
<p>If you did not create an account, then ignore this email.</p></div>`;
await sendEmail(to, subject, text, html);
};

/**
* Send email verification after registration
* @param {string} to
* @param {string} token
* @param {string} name
* @returns {Promise<void>}
*/
export const sendSuccessfulRegistration = async (to: string, token: string, name: string): Promise<void> => {
const subject = 'Email Verification';
// replace this url with the link to the email verification page of your front-end app
const verificationEmailUrl = `http://link-to-app/verify-email?token=${token}`;
const text = `Hi ${name},
Congratulations! Your account has been created.
You are almost there. Complete the final step by verifying your email at: ${verificationEmailUrl}
Don't hesitate to contact us if you face any problems
Regards,
Team`;
const html = `<div style="margin:30px; padding:30px; border:1px solid black; border-radius: 20px 10px;"><h4><strong>Hi ${name},</strong></h4>
<p>Congratulations! Your account has been created.</p>
<p>You are almost there. Complete the final step by verifying your email at: ${verificationEmailUrl}</p>
<p>Don't hesitate to contact us if you face any problems</p>
<p>Regards,</p>
<p><strong>Team</strong></p></div>`;
await sendEmail(to, subject, text, html);
};

/**
* Send email verification after registration
* @param {string} to
* @param {string} name
* @returns {Promise<void>}
*/
export const sendAccountCreated = async (to: string, name: string): Promise<void> => {
const subject = 'Account Created Successfully';
// replace this url with the link to the email verification page of your front-end app
const loginUrl = `http://link-to-app/auth/login`;
const text = `Hi ${name},
Congratulations! Your account has been created successfully.
You can now login at: ${loginUrl}
Don't hesitate to contact us if you face any problems
Regards,
Team`;
const html = `<div style="margin:30px; padding:30px; border:1px solid black; border-radius: 20px 10px;"><h4><strong>Hi ${name},</strong></h4>
<p>Congratulations! Your account has been created successfully.</p>
<p>You can now login at: ${loginUrl}</p>
<p>Don't hesitate to contact us if you face any problems</p>
<p>Regards,</p>
<p><strong>Team</strong></p></div>`;
await sendEmail(to, subject, text, html);
};
44 changes: 44 additions & 0 deletions src/Users/user.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import httpStatus from 'http-status';
import { Request, Response } from 'express';
import mongoose from 'mongoose';
import catchAsync from '../utils/catchAsync';
import ApiError from '../utils/ApiError';
import pick from '../utils/pick';
import { createUser, queryUsers, getUserById, updateUserById, deleteUserById } from './user.service';
import { IOptions } from '../plugins/paginate';

export const createUserController = catchAsync(async (req: Request, res: Response) => {
const user = await createUser(req.body);
res.status(httpStatus.CREATED).send(user);
});

export const getUsers = catchAsync(async (req: Request, res: Response) => {
const filter = pick(req.query, ['name', 'role']);
const options: IOptions = pick(req.query, ['sortBy', 'limit', 'page']);
const result = await queryUsers(filter, options);
res.send(result);
});

export const getUser = catchAsync(async (req: Request, res: Response) => {
if (typeof req.params['userId'] === 'string') {
const user = await getUserById(new mongoose.Schema.Types.ObjectId(req.params['userId']));
if (!user) {
throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
}
res.send(user);
}
});

export const updateUser = catchAsync(async (req: Request, res: Response) => {
if (typeof req.params['userId'] === 'string') {
const user = await updateUserById(new mongoose.Schema.Types.ObjectId(req.params['userId']), req.body);
res.send(user);
}
});

export const deleteUser = catchAsync(async (req: Request, res: Response) => {
if (typeof req.params['userId'] === 'string') {
await deleteUserById(new mongoose.Schema.Types.ObjectId(req.params['userId']));
res.status(httpStatus.NO_CONTENT).send();
}
});
4 changes: 2 additions & 2 deletions src/Users/user.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export interface IUser {
name: string;
email: string;
password: string;
role?: string;
isEmailVerified?: boolean;
role: string;
isEmailVerified: boolean;
}

export interface IUserDoc extends IUser, Document {
Expand Down
5 changes: 5 additions & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ export default {
refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS,
resetPasswordExpirationMinutes: envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES,
verifyEmailExpirationMinutes: envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES,
cookieOptions: {
httpOnly: true,
secure: envVars.NODE_ENV === 'production',
signed: true,
},
},
email: {
smtp: {
Expand Down
2 changes: 1 addition & 1 deletion src/config/passport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { IPayload } from '../Tokens/token.interfaces';
const cookieExtractor = function (req: Request): string {
let token = null;
if (req && req.cookies) {
token = req.cookies.jwt;
token = req.cookies.accessToken;
}
return token;
};
Expand Down
8 changes: 8 additions & 0 deletions src/declaration.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
import { IUserDoc } from './Users/user.interfaces';

declare module 'express' {
export interface Request {
user: IUserDoc;
}
}

declare module 'xss-clean';
7 changes: 4 additions & 3 deletions src/middlewares/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,20 @@ import passport from 'passport';
import httpStatus from 'http-status';
import ApiError from '../utils/ApiError';
import { roleRights } from '../config/roles';
import { IUserDoc } from '../Users/user.interfaces';

const verifyCallback =
(req: Request, resolve: any, reject: any, requiredRights: string[]) =>
async (err: Error, user: Record<string, any>, info: string) => {
async (err: Error, user: IUserDoc, info: string) => {
if (err || info || !user) {
return reject(new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate'));
}
req.user = user;

if (requiredRights.length) {
const userRights = roleRights?.get(user['role']);
const userRights = roleRights.get(user.role);
const hasRequiredRights = requiredRights.every((requiredRight: string) => userRights?.includes(requiredRight));
if (!hasRequiredRights && req.params['userId'] !== user['id']) {
if (!hasRequiredRights && req.params['userId'] !== user.id) {
return reject(new ApiError(httpStatus.FORBIDDEN, 'Forbidden'));
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
"typeRoots": [
"@types",
"./node_modules/@types",
], /* Specify multiple folders that act like `./node_modules/@types`. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true, /* Enable importing .json files */
Expand Down

0 comments on commit cb09ae3

Please sign in to comment.