Let's create the middleware to handle the authentications. Create this file src/middlewares/jwt.middleware.ts
.
This middleware takes the Authorization
header (Bearer [token]) and get the token. jsonwebtoken
packages handles the process of validating the token behind the scene.
If the token doesn't exist, the access is denied (401 Error). Else, we validated the token, the package handles the token and return an error if it's expired or invalid (in case not expired, then it's invalid). Otherwise, it's valid, and we move on to the next handler (The route handler probably).
The IRequest
interface is a globally declared interface, it extends default Express Request, and add user payload (userId
). We can use this userId
in other route handler to get the logged in user. Create src/index.d.ts
As I mentioned in the Architecture section, the code will be separated, the router will only handle routing and listening. The logic will be in repositories files. Let's create the file src/routes/v1/auth.route.ts
This router depends on 3 other files; The types and ZOD schema, the ZOD validator middleware, and the Auth Repo.
All middlewares are injected in src/middlewares
directory. So, let's create the middleware. Create validateRequest.middleware.ts
file.
This middleware is simple. ZOD validates the schema passed through the middleware in Router handler. If it's valid, move to next handler. Otherwise, return a 422 response with the ZOD error to know which property isn't valid and what's the error. That's the role of parseZodErrors
. It parses the ZodErrors
to the ApiResponseBody
message property.
I used a class with static ZOD schema object to have a clean import section. And used .d.ts
to declare the types. So, we got two files; the schema objects file, and type definitions file. Create a file src/schemas/auth/auth.schema.ts
Now create the definition file src/schemas/auth/auth.schema.d.ts
This definition is already declared in index.d.ts
. We won't need to import each type in each file we use.
Before we start with the repository, we need to create the prisma models and migrate them. Create the schema in prisma/schema.prisma
Now we migrate our schema. Make sure you created the DB and added the URL in .env
And because we will handle also the TEST DB, I created a script to migrate the migrations in that DB. Create the file in prisma/scripts/migrate.ts
.
Install cross-env
before, and make sure you installed ts-node
like mentioned before.
After that, you can run the script
import { Response, NextFunction } from 'express';
import jwt, { TokenExpiredError } from 'jsonwebtoken';
import { ResponseHandler } from '@/utils/responseHandler';
import appConfig from '@/config/app.config';
export function authenticateJWT(req: IRequest, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (authHeader) {
const token = authHeader.split(' ')[1];
jwt.verify(token, appConfig.jwt.secret, (err, user) => {
if (err || !user) {
if (err instanceof TokenExpiredError) {
const resBody = ResponseHandler.Unauthorized('Unauthenticated');
res.status(resBody.error!.code).json(resBody);
} else {
const resBody = ResponseHandler.Forbidden('Access forbidden: Invalid token');
res.status(resBody.error!.code).json(resBody);
}
return;
}
req.user = user;
next();
});
} else {
const resBody = ResponseHandler.Unauthorized('Access denied: No token provided');
res.status(resBody.error!.code).json(resBody);
}
}
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserType {
DOCTOR
PATIENT
ADMIN
}
model User {
id String @id @unique @default(uuid())
name String
email String @unique
phone String @unique
password String
verifiedEmail Boolean @default(false)
userType UserType
refreshToken RefreshToken[]
resetPasswordToken ResetPasswordToken?
updatePasswordToken UpdatePasswordToken?
verifyEmailToken VerifyEmailToken?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
}
model ResetPasswordToken {
id String @id @unique @default(uuid())
token String @unique
expiresAt DateTime
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("reset_password_tokens")
}
model UpdatePasswordToken {
id String @id @unique @default(uuid())
token String @unique
newPassword String
expiresAt DateTime
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("update_password_tokens")
}
model VerifyEmailToken {
id String @id @unique @default(uuid())
token String @unique
expiresAt DateTime
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("verify_email_tokens")
}
model RefreshToken {
id String @id @default(uuid())
token String @unique
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expiresAt DateTime
createdAt DateTime @default(now())
@@map("refresh_tokens")
}
// Refrence files, to import the types from other definition files. We will be importing them here instead of importing the types in each file.
/// <reference path="./schemas/auth/auth.schema.d.ts" />
/// <reference path="./types/auth.d.ts" />
import { Request as ExpressRequest } from 'express';
declare global {
interface IRequest extends ExpressRequest {
user?: any;
}
}
import { NextFunction, Request, Response, Router } from 'express';
import { validate } from '@/middlewares/validateRequest.middleware';
import { AuthRepository } from '@/repositories/auth.repo';
import { authenticateJWT } from '@/middlewares/jwt.middleware';
import HttpStatusCode from '@/utils/HTTPStatusCodes';
import { AuthZODSchema } from '@/schemas/auth.schema';
const AuthRoutes = Router();
AuthRoutes.post(
'/login',
validate(AuthZODSchema.authSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TAuthSchema = req.body;
const resBody = await AuthRepository.loginUser(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/refresh-token',
validate(AuthZODSchema.refreshTokenSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TRefreshTokenSchema = req.body;
const resBody = await AuthRepository.refreshToken(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/register',
validate(AuthZODSchema.registerSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TRegisterSchema = req.body;
const resBody = await AuthRepository.createUser(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/forget-password',
validate(AuthZODSchema.forgetPasswordSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TForgetPasswordSchema = req.body;
const resBody = await AuthRepository.forgotPassword(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/reset-password',
validate(AuthZODSchema.resetPasswordSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TResetPasswordSchema = req.body;
const resBody = await AuthRepository.resetPassword(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/update-password',
authenticateJWT,
validate(AuthZODSchema.updatePasswordSchema),
async (req: IRequest, res: Response, next: NextFunction) => {
try {
const body: TUpdatePasswordSchema = req.body;
// We get the userId from the request. It's passed by "authenticateJWT"
const resBody = await AuthRepository.updatePassword(body, req.user.userId);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/confirm-update-password',
validate(AuthZODSchema.validateUserSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TValidateUserSchema = req.body;
const resBody = await AuthRepository.confirmUpdatePassword(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
AuthRoutes.post(
'/verify-user',
validate(AuthZODSchema.validateUserSchema),
async (req: Request, res: Response, next: NextFunction) => {
try {
const body: TValidateUserSchema = req.body;
const resBody = await AuthRepository.verifyUser(body);
res.status(resBody.error ? resBody.error.code : HttpStatusCode.OK).json(resBody);
next();
} catch (err) {
next(err);
}
}
);
export default AuthRoutes;
import { Request, Response, NextFunction } from 'express';
import { ZodError, ZodSchema } from 'zod';
import { ResponseHandler } from '@/utils/responseHandler';
function parseZodErrors(errors: ZodError) {
return errors.errors.map((err) => `${err.path.join(', ')}: ${err.message}`);
}
export function validate(schema: ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error: any) {
const resBody = ResponseHandler.InvalidBody({
message: 'Validation Error',
errors: parseZodErrors(error),
});
res.status(resBody.error!.code).json(resBody);
}
};
}
import { z } from 'zod';
export class AuthZODSchema {
static readonly authSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
type: z.enum(['DOCTOR', 'PATIENT', 'ADMIN']),
});
static readonly registerSchema = z.object({
name: z.string().min(2),
phone: z.string().refine((phone) => /^\+\d{10,15}$/.test(phone), 'Invalid phone number'),
email: z.string().email(),
password: z.string().min(8),
type: z.enum(['DOCTOR', 'PATIENT']),
});
static readonly refreshTokenSchema = z.object({
refreshToken: z.string().uuid(),
});
static readonly forgetPasswordSchema = z.object({
email: z.string().email(),
type: z.enum(['DOCTOR', 'PATIENT', 'ADMIN']),
});
static readonly resetPasswordSchema = z.object({
newPassword: z.string().min(8),
token: z.string().uuid(),
});
static readonly validateUserSchema = z.object({
token: z.string().uuid(),
});
static readonly updatePasswordSchema = z.object({
oldPassword: z.string().min(8),
newPassword: z.string().min(8),
type: z.enum(['DOCTOR', 'PATIENT', 'ADMIN']),
});
}
import { AuthZODSchema } from './auth.schema';
import { z } from 'zod';
declare global {
type TAuthSchema = z.infer<typeof AuthZODSchema.authSchema>;
type TRegisterSchema = z.infer<typeof AuthZODSchema.registerSchema>;
type TForgetPasswordSchema = z.infer<typeof AuthZODSchema.forgetPasswordSchema>;
type TResetPasswordSchema = z.infer<typeof AuthZODSchema.resetPasswordSchema>;
type TUpdatePasswordSchema = z.infer<typeof AuthZODSchema.updatePasswordSchema>;
type TValidateUserSchema = z.infer<typeof AuthZODSchema.validateUserSchema>;
type TRefreshTokenSchema = z.infer<typeof AuthZODSchema.refreshTokenSchema>;
}
import { execSync } from 'child_process';
import { config } from 'dotenv';
config()
async function runMigrations() {
try {
console.log('Running migrations...');
execSync(`cross-env DATABASE_URL=${process.env.TEST_DATABASE_URL} npx prisma migrate deploy`, {
stdio: 'inherit',
});
console.log('Migrations completed successfully.');
} catch (error) {
console.error('Error running migrations:', error);
process.exit(1);
}
}
runMigrations()
npx prisma migrate dev --name init
npm i -D cross-env
npx ts-node prisma/scripts/migrate.ts