diff --git a/server/package.json b/server/package.json index b2f8e766..76885a40 100644 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/index.ts b/server/src/index.ts index 43315ff9..a1849cd5 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -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(); @@ -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); diff --git a/server/src/infrastructure/database/repositories/OtpRepository.ts b/server/src/infrastructure/database/repositories/OtpRepository.ts index 98dbb30d..4d62bff6 100644 --- a/server/src/infrastructure/database/repositories/OtpRepository.ts +++ b/server/src/infrastructure/database/repositories/OtpRepository.ts @@ -11,7 +11,7 @@ export default class OtpRepository implements IOtpRepository { return await this.model.findOne({ otp, email }); } - async delete(otp: number, email: string): Promise { - await this.model.findOneAndDelete({otp,email}); + async deleteMany(otp: number, email: string): Promise { + await this.model.deleteMany({otp,email}); } } diff --git a/server/src/infrastructure/services/EmailService.ts b/server/src/infrastructure/services/EmailService.ts index 15a48a9e..78333646 100644 --- a/server/src/infrastructure/services/EmailService.ts +++ b/server/src/infrastructure/services/EmailService.ts @@ -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); } } diff --git a/server/src/infrastructure/services/TokenService.ts b/server/src/infrastructure/services/TokenService.ts new file mode 100644 index 00000000..57292a78 --- /dev/null +++ b/server/src/infrastructure/services/TokenService.ts @@ -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 }; + } + +} diff --git a/server/src/interface/repositories/IOtpRepository.ts b/server/src/interface/repositories/IOtpRepository.ts index 139ee458..07cc5b1c 100644 --- a/server/src/interface/repositories/IOtpRepository.ts +++ b/server/src/interface/repositories/IOtpRepository.ts @@ -3,5 +3,5 @@ import IOtp from "../../domain/entities/IOtp"; export default interface IOtpRepository { create(otp: number, email: string): Promise; findOne(otp: number, email: string): Promise; - delete(otp: number, email: string): Promise; + deleteMany(otp: number, email: string): Promise; } diff --git a/server/src/interface/services/ITokenService.ts b/server/src/interface/services/ITokenService.ts new file mode 100644 index 00000000..121d6bb3 --- /dev/null +++ b/server/src/interface/services/ITokenService.ts @@ -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 }; +} diff --git a/server/src/presentation/controllers/PatientController.ts b/server/src/presentation/controllers/PatientController.ts index ab66491a..9ae33d10 100644 --- a/server/src/presentation/controllers/PatientController.ts +++ b/server/src/presentation/controllers/PatientController.ts @@ -7,7 +7,6 @@ import { isValidatePassword, isValidEmail } from "../validators/authValidators"; export default class PatientController { constructor( - private patientUseCase: UpdatePatientUseCase, private registerPatientUseCase: RegisterPatientUseCase, private loginPatientUseCase: LoginPatientUseCase ) {} @@ -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); - } } } @@ -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); } } diff --git a/server/src/presentation/middlewares/PatientAuthMiddleware.ts b/server/src/presentation/middlewares/PatientAuthMiddleware.ts new file mode 100644 index 00000000..69c09379 --- /dev/null +++ b/server/src/presentation/middlewares/PatientAuthMiddleware.ts @@ -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" }); + } + }; +} diff --git a/server/src/presentation/middlewares/errorHandler.ts b/server/src/presentation/middlewares/errorHandler.ts index a50ca9ff..da632035 100644 --- a/server/src/presentation/middlewares/errorHandler.ts +++ b/server/src/presentation/middlewares/errorHandler.ts @@ -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 }), diff --git a/server/src/presentation/routers/PatientRoutes.ts b/server/src/presentation/routers/PatientRoutes.ts deleted file mode 100644 index 1a3f7eb2..00000000 --- a/server/src/presentation/routers/PatientRoutes.ts +++ /dev/null @@ -1,34 +0,0 @@ -import express from "express"; -import PatientRepository from "../../infrastructure/database/repositories/PatientRepository"; -import PasswordService from "../../infrastructure/services/PasswordService"; -import PatientUseCase from "../../use_case/patient/UpdatePatientUseCase"; -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"; - -const route = express(); - -const emailService = new EmailService(); -const otpRepository = new OtpRepository(); -const passwordService = new PasswordService(); -const patientRepository = new PatientRepository(); -const patientUseCase = new PatientUseCase(patientRepository); -const registerPatientUseCase = new RegisterPatientUseCase(patientRepository, passwordService); -const loginPatientUseCase = new LoginPatientUseCase(patientRepository, passwordService, emailService, otpRepository); -const patientController = new PatientController(patientUseCase, registerPatientUseCase, loginPatientUseCase); - -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); -}); - -export default route; diff --git a/server/src/presentation/routers/index.ts b/server/src/presentation/routers/index.ts index c6a7e162..740b0b79 100644 --- a/server/src/presentation/routers/index.ts +++ b/server/src/presentation/routers/index.ts @@ -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); diff --git a/server/src/presentation/routers/patient/PatientAuthRoutes.ts b/server/src/presentation/routers/patient/PatientAuthRoutes.ts new file mode 100644 index 00000000..18013fc9 --- /dev/null +++ b/server/src/presentation/routers/patient/PatientAuthRoutes.ts @@ -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; diff --git a/server/src/types/index.ts b/server/src/types/index.ts new file mode 100644 index 00000000..199ba2b7 --- /dev/null +++ b/server/src/types/index.ts @@ -0,0 +1,8 @@ +import { Request } from "express"; + +export interface CustomRequest extends Request { + patient?: { + email: string; + id: string; + }; +} diff --git a/server/src/use_case/patient/LoginPatientUseCase.ts b/server/src/use_case/patient/LoginPatientUseCase.ts index 91f28dc9..83a7b553 100644 --- a/server/src/use_case/patient/LoginPatientUseCase.ts +++ b/server/src/use_case/patient/LoginPatientUseCase.ts @@ -3,6 +3,7 @@ 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 { @@ -10,17 +11,18 @@ export default class LoginPatientUseCase { 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!); @@ -30,14 +32,35 @@ export default class LoginPatientUseCase { return { email: foundedPatient.email! }; } - async validateOtp(otp: number, email: string): Promise { + 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 }; } } diff --git a/server/src/use_case/patient/RegisterPatientUseCase.ts b/server/src/use_case/patient/RegisterPatientUseCase.ts index a490a717..462dfacd 100644 --- a/server/src/use_case/patient/RegisterPatientUseCase.ts +++ b/server/src/use_case/patient/RegisterPatientUseCase.ts @@ -11,6 +11,6 @@ export default class RegisterPatientUseCase { async execute(patient: IPatient) { patient.password = await this.passwordService.hash(patient.password!); const {_id} =await this.patientRepository.create(patient); - return `New Patient with ${_id} Created` + return `New Patient with id ${_id} Created` } }