Skip to content

Commit

Permalink
Implement JWT authentication with access & refresh tokens, OTP handli…
Browse files Browse the repository at this point in the history
…ng, and auth middleware
  • Loading branch information
sinanptm committed Aug 28, 2024
1 parent 15325ae commit 23d59ce
Show file tree
Hide file tree
Showing 16 changed files with 210 additions and 61 deletions.
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
Expand Down
2 changes: 2 additions & 0 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import dotenv from "dotenv";
import { connectDB } from "./config/connectDB";
import routes from "./presentation/routers/index";
import cors from "cors";
import cookieParser from "cookie-parser";

dotenv.config();

Expand All @@ -18,6 +19,7 @@ app.use(
credentials: true,
})
);
app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/api", routes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default class OtpRepository implements IOtpRepository {
return await this.model.findOne({ otp, email });
}

async delete(otp: number, email: string): Promise<void> {
await this.model.findOneAndDelete({otp,email});
async deleteMany(otp: number, email: string): Promise<void> {
await this.model.deleteMany({otp,email});
}
}
3 changes: 1 addition & 2 deletions server/src/infrastructure/services/EmailService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ export default class EmailService implements IEmailService{
},
});

const info = await transporter.sendMail({
await transporter.sendMail({
from: process.env.SENDER_MAIL,
to: email,
subject: 'Your OTP for Verification',
html: htmlTemplate,
});

console.log('Email sent:', info.messageId);
}

}
34 changes: 34 additions & 0 deletions server/src/infrastructure/services/TokenService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import ITokenService from "../../interface/services/ITokenService";
import jwt, { JwtPayload } from "jsonwebtoken";

export default class TokenService implements ITokenService {
private signToken(payload:object,secret:string,expiresIn:string):string{
return jwt.sign(payload,secret,{expiresIn});
}
private verifyToken(token:string,secret:string):JwtPayload{
try {
return jwt.verify(token,secret) as JwtPayload
} catch (error) {
throw new Error("Invalid token")
}
}

createRefreshToken(email: string, id: string): string {
return this.signToken({ email, id }, process.env.REFRESH_TOKEN_SECRET!, "7d");
}

verifyRefreshToken(token: string): { email: string; id: string } {
const decoded = this.verifyToken(token, process.env.REFRESH_TOKEN_SECRET!);
return { email: decoded.email, id: decoded.id };
}

createAccessToken(email: string, id: string): string {
return this.signToken({ email, id }, process.env.ACCESS_TOKEN_SECRET!, "15m");
}

verifyAccessToken(token: string): { email: string; id: string } {
const decoded = this.verifyToken(token, process.env.ACCESS_TOKEN_SECRET!);
return { email: decoded.email, id: decoded.id };
}

}
2 changes: 1 addition & 1 deletion server/src/interface/repositories/IOtpRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import IOtp from "../../domain/entities/IOtp";
export default interface IOtpRepository {
create(otp: number, email: string): Promise<void>;
findOne(otp: number, email: string): Promise<IOtp | null>;
delete(otp: number, email: string): Promise<void>;
deleteMany(otp: number, email: string): Promise<void>;
}
8 changes: 8 additions & 0 deletions server/src/interface/services/ITokenService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { IPatient } from "../../domain/entities/Patient";

export default interface ITokenService {
createRefreshToken(email: string, id: string): string;
verifyRefreshToken(token: string): { email: string; id: string };
createAccessToken(email: string, id: string): string;
verifyAccessToken(token: string): { email: string; id: string };
}
49 changes: 37 additions & 12 deletions server/src/presentation/controllers/PatientController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { isValidatePassword, isValidEmail } from "../validators/authValidators";

export default class PatientController {
constructor(
private patientUseCase: UpdatePatientUseCase,
private registerPatientUseCase: RegisterPatientUseCase,
private loginPatientUseCase: LoginPatientUseCase
) {}
Expand Down Expand Up @@ -55,15 +54,7 @@ export default class PatientController {
res.status(204).json({ message: "No further action required" });
}
} catch (error: any) {
if (error.message === "User Not Found") {
return res.status(404).json({ message: "User not found" });
} else if (error.message === "Invalid Credentials") {
return res.status(401).json({ message: "Invalid credentials" });
} else if (error.message === "Unauthorized") {
return res.status(401).json({ message: "Unauthorized" });
}else{
next(error);
}
}
}

Expand All @@ -74,11 +65,45 @@ export default class PatientController {
if (!otp) return res.status(400).json({ message: "Otp is required" });
if (!email) return res.status(400).json({ message: "Email is required" });

const patient = await this.loginPatientUseCase.validateOtp(otp, email);
const { refreshToken, accessToken } = await this.loginPatientUseCase.validateOtp(otp, email);

res.cookie("patient_token", refreshToken, {
httpOnly: true,
secure: true,
sameSite: "strict" as const,
maxAge: 30 * 24 * 60 * 1000,
});

res.json({ accessToken });
} catch (error: any) {
next(error);
}
}

async refreshAccessToken(req: Request, res: Response, next: NextFunction) {
try {
const cookies = req.cookies;
if (!cookies?.patient_token) return res.status(401).json({ message: "Unauthorized" });

const newAccessToken = await this.loginPatientUseCase.refreshAccessToken(cookies.patient_token);

res.status(200).json(patient);
return res.status(200).json(newAccessToken);
} catch (error: any) {
if (error.message === "Invalid Otp") return res.status(401).json({ message: "Invalid Otp" });
next(error);
}
}

logout(req: Request, res: Response, next: NextFunction) {
try {
const cookies = req.cookies;
if (!cookies?.patient_token) return res.sendStatus(204);
res.clearCookie("patient_token", {
httpOnly: true,
sameSite: "strict" as const,
secure: true,
});
res.json({ message: "cookie cleared" });
} catch (error) {
next(error);
}
}
Expand Down
33 changes: 33 additions & 0 deletions server/src/presentation/middlewares/PatientAuthMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { NextFunction, Response } from "express";
import ITokenService from "../../interface/services/ITokenService";
import { CustomRequest } from "../../types";

export default class PatientAuthMiddleware {
constructor(private tokenService: ITokenService) {}

exec = (req: CustomRequest, res: Response, next: NextFunction) => {
try {
const authHeader = req.headers.authorization || req.headers.Authorization;
const tokenString = Array.isArray(authHeader) ? authHeader[0] : authHeader;

if (!tokenString?.startsWith("Bearer ")) {
return res.status(401).json({ message: "Unauthorized: No or invalid token provided" });
}

const token = tokenString.split(" ")[1];

if (!token) {
return res.status(401).json({ message: "Unauthorized: Token is missing" });
}

const decodedToken = this.tokenService.verifyAccessToken(token);
req.patient = {
email: decodedToken.email,
id: decodedToken.id,
};
next();
} catch (error) {
res.status(401).json({ message: "Forbidden" });
}
};
}
8 changes: 8 additions & 0 deletions server/src/presentation/middlewares/errorHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export const errorHandler = (err: any, req: Request, res: Response, next: NextFu
});
}

if (err.message === "Unauthorized" || err.message === "Invalid Credentials" || err.message === "Invalid Otp") {
return res.status(401).json({ message: err.message });
} else if (err.message === "Patient is blocked") {
return res.status(403).json({ message: err.message });
}else if (err.message==="Patient not found"){
return res.status(404).json({message:err.message})
}

res.status(statusCode).json({
message: err.message || "Internal Server Error",
...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
Expand Down
34 changes: 0 additions & 34 deletions server/src/presentation/routers/PatientRoutes.ts

This file was deleted.

4 changes: 2 additions & 2 deletions server/src/presentation/routers/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import express from "express";
import patientRoutes from "./PatientRoutes";
import patientAuthRoutes from "./patient/PatientAuthRoutes";
import { errorHandler } from "../middlewares/errorHandler";

const app = express();

app.use("/patient", patientRoutes);
app.use("/patient", patientAuthRoutes);

app.use(errorHandler);

Expand Down
42 changes: 42 additions & 0 deletions server/src/presentation/routers/patient/PatientAuthRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import express from "express";
import PatientRepository from "../../../infrastructure/database/repositories/PatientRepository";
import PasswordService from "../../../infrastructure/services/PasswordService";
import RegisterPatientUseCase from "../../../use_case/patient/RegisterPatientUseCase";
import PatientController from "../../controllers/PatientController";
import LoginPatientUseCase from "../../../use_case/patient/LoginPatientUseCase";
import EmailService from "../../../infrastructure/services/EmailService";
import OtpRepository from "../../../infrastructure/database/repositories/OtpRepository";
import TokenService from "../../../infrastructure/services/TokenService";
import PatientAuthMiddleware from "../../middlewares/PatientAuthMiddleware";

const route = express();

const emailService = new EmailService();
const tokenService = new TokenService();
const otpRepository = new OtpRepository();
const passwordService = new PasswordService();
const patientRepository = new PatientRepository();
const registerPatientUseCase = new RegisterPatientUseCase(patientRepository, passwordService);
const loginPatientUseCase = new LoginPatientUseCase(patientRepository, passwordService, emailService, otpRepository,tokenService);
const patientController = new PatientController(registerPatientUseCase, loginPatientUseCase);

const patientAuthMiddleWare = new PatientAuthMiddleware (tokenService);

route.post("/", (req, res, next) => {
patientController.register(req, res, next);
});
route.post("/login", (req, res, next) => {
patientController.login(req, res, next);
});
route.post("/otp-verification", (req, res, next) => {
patientController.validateOtp(req, res, next);
});
route.get("/refresh",(req,res,next)=>{
patientController.refreshAccessToken(req,res,next);
});
route.post('/logout',patientAuthMiddleWare.exec,(req,res,next)=>{
patientController.logout(req,res,next)
});


export default route;
8 changes: 8 additions & 0 deletions server/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Request } from "express";

export interface CustomRequest extends Request {
patient?: {
email: string;
id: string;
};
}
37 changes: 30 additions & 7 deletions server/src/use_case/patient/LoginPatientUseCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,26 @@ import IOtpRepository from "../../interface/repositories/IOtpRepository";
import IPatientRepository from "../../interface/repositories/IPatientRepository";
import IEmailService from "../../interface/services/IEmailService";
import { IPasswordServiceRepository } from "../../interface/services/IPasswordServiceRepository";
import ITokenService from "../../interface/services/ITokenService";
import { generateOTP } from "../../utils";

export default class LoginPatientUseCase {
constructor(
private patientRepository: IPatientRepository,
private passwordService: IPasswordServiceRepository,
private emailService: IEmailService,
private otpRepository: IOtpRepository
private otpRepository: IOtpRepository,
private tokenService: ITokenService
) {}

async execute(patient: IPatient): Promise<{ email: string } | null> {
const foundedPatient = await this.patientRepository.findByEmailWithPassword(patient.email!);
if (!foundedPatient) throw new Error("User Not Found");
if (!foundedPatient) throw new Error("Patient Not Found");

const isPasswordValid = await this.passwordService.compare(patient.password!, foundedPatient.password!);
if (!isPasswordValid) throw new Error("Invalid Credentials");

if(foundedPatient.isBlocked) throw new Error("Unauthorized")
if (foundedPatient.isBlocked) throw new Error("Unauthorized");

const otp = generateOTP(6);
await this.otpRepository.create(otp, foundedPatient.email!);
Expand All @@ -30,14 +32,35 @@ export default class LoginPatientUseCase {
return { email: foundedPatient.email! };
}

async validateOtp(otp: number, email: string): Promise<IPatient> {
async validateOtp(otp: number, email: string): Promise<{ accessToken: string; refreshToken: string }> {
const isOtp = await this.otpRepository.findOne(otp, email);
if (!isOtp) throw Error("Invalid Otp");

const patient = await this.patientRepository.findByEmail(email);
const patient = await this.patientRepository.findByEmail(email)!;
if (patient && patient?.isBlocked) throw new Error("Unauthorized");

patient!.token = '2dfjlaasdfsadfa'
const refreshToken = this.tokenService.createRefreshToken(patient?.email!, patient?._id!);
const accessToken = this.tokenService.createAccessToken(patient?.email!, patient?._id!);

return patient!
patient!.token = refreshToken;

await this.patientRepository.update(patient!);

await this.otpRepository.deleteMany(otp,email);

return { accessToken, refreshToken };
}

async refreshAccessToken(token: string): Promise<{ accessToken: string }> {
const { id } = this.tokenService.verifyRefreshToken(token);

const patient = await this.patientRepository.findById(id);
if (!patient) throw new Error("Unauthorized");

if (patient.isBlocked) throw new Error("Patient is blocked");

const accessToken = this.tokenService.createAccessToken(patient.email!, patient._id!);

return { accessToken };
}
}
Loading

1 comment on commit 23d59ce

@vercel
Copy link

@vercel vercel bot commented on 23d59ce Aug 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.