Finally, we arrived to the core logic of our project. Create now src/repositories/auth.repo.ts
. In this file we gonna create a static methods class for our repo.
Before that, Let's add some other functions and services we gonna need here. We will need:
Create this file src/utils/jwtHandler.ts
In the JWT Middleware, the user object in the call back is the payload we created. So, in our config, the user will return the user id and a timestamp.
This services calls nodemailer
to send the email. Create src/services/mail.service.ts
install first nodemailer
As you can see, we have another config file for emails. Create src/config/mail.config.ts
Note: We already added the .env
variables for the mailer.
A helper functions we can use in our app. Create src/utils/helpers.ts
First, we will go with each function alone, then have the summary file.
You can throw "Credentials Error" for password not match error as well if you want.
When the access token has expired, the client side calls to refresh that access token. And we use this "Refresh token" as validation process, also to know which user is it. Note that refresh token has an expiration date too (double of JWT token).
This is a private method, called by CreateUser
method.
Notice that the a
tag have an ID of token-link
. We will use that later for testing, it's important for email testing.
The token passed to this method is received from the sent email
After the user receives the email and is redirected to the client side. The latter sends us the token. The process is the almost the same as the previous token validation processes.
We walked through all the methods. Here is the whole file
import appConfig from '@/config/app.config';
import jwt from 'jsonwebtoken';
import { v4 as uuidv4 } from 'uuid';
export const generateAccessToken = (userId: string) => {
return jwt.sign( // This payload is the object returned in JWT middleware (check the next code snippet)
{
userId,
timestamp: Date.now(),
},
appConfig.jwt.secret,
{ expiresIn: appConfig.jwt.expiresIn } // set an expiration period in the appConfig
);
};
// This function generated a UUID for the refresh token sent at login
export const generateRefreshToken = () => {
return uuidv4();
};
// jsonwbtoken verification methods. Check their docs for further information
export const verifyAccessToken = (token: string) => {
return jwt.verify(token, appConfig.jwt.secret);
};
export const verifyRefreshToken = (token: string) => {
return jwt.verify(token, appConfig.jwt.refreshSecretKey);
};
// src/middlewares/jwt.middleware.ts
jwt.verify(token, appConfig.jwt.secret, (err, **user**) => {
// ...
});
import mailConfig from '@/config/mail.config';
import nodemailer from 'nodemailer';
const transporter = nodemailer.createTransport({
host: mailConfig.mailHost,
port: mailConfig.mailPort,
auth: {
user: mailConfig.mailUser,
pass: mailConfig.mailPass,
},
});
export async function sendEmail(payload: { receivers: string[]; subject: string; html: string }) {
const info = await transporter.sendMail({
from: `"${mailConfig.mailFromName}" <${mailConfig.mailFromEmail}>`,
to: payload.receivers.join(', '),
subject: payload.subject,
html: payload.html,
});
return info;
}
import { config } from 'dotenv';
config();
const mailConfig = {
mailFromEmail: process.env.MAIL_FROM_EMAIL!,
mailFromName: process.env.MAIL_FROM_NAME!,
mailHost: process.env.MAIL_HOST!,
mailPort: Number(process.env.MAIL_PORT!),
mailUser: process.env.MAIL_USER!,
mailPass: process.env.MAIL_PASS!,
};
export default mailConfig;
// We will use it in testing
export default async function wait(time: number) {
await new Promise((r) => setTimeout(r, time));
}
export function addTime(value: number, unit: 'ms' | 's' | 'm' | 'h' | 'd', start?: Date) {
let addedValue = value;
switch (unit) {
case 's':
addedValue *= 1000;
break;
case 'm':
addedValue *= 60 * 1000;
break;
case 'h':
addedValue *= 60 * 60 * 1000;
break;
case 'd':
addedValue *= 24 * 60 * 60 * 1000;
break;
}
const initValue = start ? start.getTime() : Date.now();
return new Date(initValue + addedValue);
}
static async loginUser(payload: TAuthSchema): Promise<ApiResponseBody<IAuthResponse>> {
const resBody = new ApiResponseBody<IAuthResponse>();
try {
// Validating the user email
const user = await prisma.user.findUnique({
where: {
email: payload.email,
},
});
if (!user) {
const resBody = ResponseHandler.Unauthorized('Credentials Error');
return resBody;
}
// Validating the password
const isValidPassword = await bcrypt.compare(payload.password, user.password);
if (isValidPassword) {
const token = generateAccessToken(user.id);
const refreshToken = generateRefreshToken();
// Creating the refresh token and storing it
await prisma.refreshToken.create({
data: {
token: refreshToken,
userId: user.id,
expiresAt: addTime(30, 'd'),
},
});
const accessToken = {
token: token,
refreshToken: refreshToken,
};
// The response body
const responseData = {
accessToken: accessToken,
user: {
id: user.id,
email: user.email,
phone: user.phone,
name: user.name,
verifiedEmail: user.verifiedEmail,
userType: user.userType,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
},
};
resBody.data = responseData;
return resBody;
} else {
// In case if password doesn't match
const resBody = ResponseHandler.Unauthorized('Password not match');
return resBody;
}
} catch (err) {
logger.error(err);
resBody.error = {
code: HttpStatusCode.INTERNAL_SERVER_ERROR,
message: String(err),
};
}
return resBody;
}
static async refreshToken({
refreshToken,
}: TRefreshTokenSchema): Promise<ApiResponseBody<IRefreshTokenResponse>> {
const resBody = new ApiResponseBody<IRefreshTokenResponse>();
try {
const storedToken = await prisma.refreshToken.findUnique({
where: { token: refreshToken },
});
if (!storedToken || new Date() > storedToken.expiresAt) {
const resBody = ResponseHandler.Unauthorized('Invalid or expired refresh token');
return resBody;
}
const newAccessToken = generateAccessToken(storedToken.userId);
const newRefreshToken = generateRefreshToken();
// Updating the refresh token and the expiration date
await prisma.refreshToken.update({
where: { token: refreshToken },
data: {
token: newRefreshToken,
expiresAt: addTime(30, 'd'),
},
});
// Return the new refresh token and access token
resBody.data = { accessToken: newAccessToken, refreshToken: newRefreshToken };
} catch (err) {
logger.error(err);
resBody.error = {
code: HttpStatusCode.INTERNAL_SERVER_ERROR,
message: String(err),
};
}
return resBody;
}
static async createUser(payload: TRegisterSchema): Promise<ApiResponseBody<IUser>> {
const resBody = new ApiResponseBody<IUser>();
try {
// Create the user in DB. In case of already existing email, Prisma will throw an error
const user = await prisma.user.create({
data: {
email: payload.email,
phone: payload.phone,
name: payload.name,
password: bcrypt.hashSync(payload.password, 10), // Hash the password
userType: payload.type,
},
});
resBody.data = {
id: user.id,
email: user.email,
phone: user.phone,
name: user.name,
verifiedEmail: user.verifiedEmail,
userType: user.userType,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
};
// In case we require user email verification, we send the email.
if (appConfig.requireVerifyEmail) {
await this.sendEmailVerification(user);
}
} catch (err) {
logger.error(err);
// We check if there is an email unique constraint error thrown by Prisma.
if (err instanceof PrismaClientKnownRequestError) {
if (
err.code === 'P2002' &&
err.meta?.target &&
Array.isArray(err.meta.target) &&
err.meta.target.includes('email')
) {
resBody.error = {
code: HttpStatusCode.CONFLICT,
message: 'Email already exists',
};
}
} else {
resBody.error = {
code: HttpStatusCode.INTERNAL_SERVER_ERROR,
message: String(err),
};
}
}
return resBody;
}
private static async sendEmailVerification(user: User) {
try {
const token = uuidv4();
await prisma.verifyEmailToken.create({
data: {
token,
userId: user.id,
expiresAt: addTime(1, 'h'), // Token expired in 1 hour
},
});
// We can use handlebars to create a beautiful HTML UI
const bodyHTML = `<h1>Verify Your Email</h1>
<p>Verify your email. The link expires after <strong>1 hour</strong>.</p>
<a id="token-link" href="${process.env.VERIFY_EMAIL_UI_URL}/${token}">Confirm Email</a><br>
or copy this link: <br>
<span>${process.env.VERIFY_EMAIL_UI_URL}/${token}</span>`;
sendEmail({
receivers: [user.email],
subject: 'Verify Email',
html: bodyHTML,
});
} catch (err) {
logger.error({ message: 'Send Email Verification Error:', error: err });
}
}
static async verifyUser(payload: TValidateUserSchema): Promise<ApiResponseBody<IStatusResponse>> {
const resBody = new ApiResponseBody<IStatusResponse>();
try {
// Find the token and get only the non-expired one.
const token = await prisma.verifyEmailToken.findUnique({
where: {
token: payload.token,
expiresAt: {
gte: new Date(),
},
},
include: {
user: true,
},
});
if (!token) {
resBody.error = {
code: HttpStatusCode.NOT_FOUND,
message: 'Invalid or expired token',
};
return resBody;
}
// Set the user to verified
await prisma.user.update({
where: {
id: token.userId,
},
data: {
verifiedEmail: true,
},
});
// Delete the token from DB after use
await prisma.verifyEmailToken.delete({
where: {
token: payload.token,
},
});
resBody.data = {
status: true,
};
} catch (err) {
logger.error(err);
resBody.error = {
code: HttpStatusCode.INTERNAL_SERVER_ERROR,
message: String(err),
};
}
return resBody;
}
static async forgotPassword(
payload: TForgetPasswordSchema
): Promise<ApiResponseBody<IStatusResponse>> {
const resBody = new ApiResponseBody<IStatusResponse>();
try {
// Check the user
const user = await prisma.user.findUnique({
where: {
email: payload.email,
userType: payload.type,
},
});
if (!user) {
resBody.error = {
code: HttpStatusCode.NOT_FOUND,
message: 'User not found',
};
return resBody;
}
// Generate a token
const token = uuidv4();
await prisma.resetPasswordToken.create({
data: {
token,
userId: user.id,
expiresAt: addTime(30, 'm'), // Valid for 30 minutes
},
});
const bodyHTML = `<h1>Reset Password</h1>
<p>Click here to reset your password:</p>
<a id="token-link" href="${process.env.RESET_PASSWORD_UI_URL}/${token}">Reset Password</a><br>
or copy this link: <br>
<span>${process.env.RESET_PASSWORD_UI_URL}/${token}</span>`;
if (user) {
sendEmail({
receivers: [user.email],
subject: 'Reset Password',
html: bodyHTML,
});
}
resBody.data = {
status: true,
};
} catch (err) {
logger.error(err);
resBody.error = {
code: HttpStatusCode.INTERNAL_SERVER_ERROR,
message: String(err),
};
}
return resBody;
}
static async resetPassword(
payload: TResetPasswordSchema
): Promise<ApiResponseBody<IStatusResponse>> {
const resBody = new ApiResponseBody<IStatusResponse>();
try {
// Look for the unexpired token
const token = await prisma.resetPasswordToken.findUnique({
where: {
token: payload.token,
expiresAt: {
gte: new Date(),
},
},
include: {
user: true,
},
});
if (!token) {
resBody.error = {
code: HttpStatusCode.FORBIDDEN,
message: 'Invalid or expired token',
};
return resBody;
}
// Hash the new password
const hashedPassword = await bcrypt.hash(payload.newPassword, 10);
// Update the password
await prisma.user.update({
where: {
id: token.userId,
},
data: {
password: hashedPassword,
},
});
// Delete the used token
await prisma.resetPasswordToken.delete({
where: {
token: payload.token,
},
});
resBody.data = {
status: true,
};
} catch (err) {
logger.error(err);
resBody.error = {
code: HttpStatusCode.INTERNAL_SERVER_ERROR,
message: String(err),
};
}
return resBody;
}
static async updatePassword(
payload: TUpdatePasswordSchema,
userId: string
): Promise<ApiResponseBody<IStatusResponse>> {
const resBody = new ApiResponseBody<IStatusResponse>();
try {
// Getting the user.
// The userId is fetched from the JWT Authentication middleware.
const user = await prisma.user.findUnique({
where: {
id: userId,
},
});
if (!user) {
resBody.error = {
code: HttpStatusCode.NOT_FOUND,
message: 'User not found',
};
return resBody;
}
// Validate the old password
const isValidPassword = await bcrypt.compare(payload.oldPassword, user.password);
if (!isValidPassword) {
resBody.error = {
code: HttpStatusCode.UNAUTHORIZED,
message: 'Invalid old password',
};
return resBody;
}
if (appConfig.updatePasswordRequireVerification) {
// In case the confirmation is required, we send the email and wait for the user to confirm it
await this.sendConfirmPasswordUpdate(user, payload.newPassword);
} else {
const hashedPassword = await bcrypt.hash(payload.newPassword, 10);
await prisma.user.update({
where: {
id: userId,
},
data: {
password: hashedPassword,
},
});
}
resBody.data = {
status: true,
};
} catch (err) {
logger.error(err);
resBody.error = {
code: HttpStatusCode.INTERNAL_SERVER_ERROR,
message: String(err),
};
}
return resBody;
}
private static async sendConfirmPasswordUpdate(user: User, newPassword: string) {
try {
const token = uuidv4();
const hashedPassword = await bcrypt.hash(newPassword, 10);
await prisma.updatePasswordToken.create({
data: {
token,
newPassword: hashedPassword, // We store the hashed password
userId: user.id,
expiresAt: addTime(1, 'h'),
},
});
const bodyHTML = `<h1>Confirm password update</h1>
<p>Confirm updating password. The link expires after <strong>1 hour</strong>.</p>
<a id="token-link" href="${process.env.CONFIRM_UPDATE_PASSWORD_EMAIL_UI_URL}/${token}">Confirm password</a><br>
or copy this link: <br>
<span>${process.env.CONFIRM_UPDATE_PASSWORD_EMAIL_UI_URL}/${token}</span>`;
sendEmail({
receivers: [user.email],
subject: 'Update Password',
html: bodyHTML,
});
} catch (err) {
logger.error({ message: 'Send password update Email Error:', error: err });
}
}
static async confirmUpdatePassword(
payload: TValidateUserSchema
): Promise<ApiResponseBody<IStatusResponse>> {
const resBody = new ApiResponseBody<IStatusResponse>();
try {
const token = await prisma.updatePasswordToken.findUnique({
where: {
token: payload.token,
},
});
if (!token) {
resBody.error = {
code: HttpStatusCode.FORBIDDEN,
message: 'Invalid or expired token',
};
return resBody;
}
// We update the user password from the stored password
await prisma.user.update({
where: {
id: token.userId,
},
data: {
password: token.newPassword,
},
});
// And finally delete the token record
await prisma.updatePasswordToken.delete({
where: {
token: payload.token,
},
});
resBody.data = {
status: true,
};
} catch (err) {
logger.error(err);
resBody.error = {
code: HttpStatusCode.INTERNAL_SERVER_ERROR,
message: String(err),
};
}
return resBody;
}
npm i nodemailer
npm i -D @types/nodemailer