diff --git a/src/domain/entities/auth.ts b/src/domain/entities/auth.ts index 471fc9d..6aefe5a 100644 --- a/src/domain/entities/auth.ts +++ b/src/domain/entities/auth.ts @@ -11,6 +11,7 @@ export interface AuthJwtResponseModel { jwt: string; jwt_refresh: string } + export interface AuthJwtRefreshedResponseModel { jwt: string; } @@ -20,10 +21,12 @@ export interface DecodedToken extends UserResponseModel, JwtPayload { } export interface CustomRequest extends Request { token: DecodedToken; } + export interface ChangeCredentialsModel { user_id: number; password: string; new_password: string; password_hash?: string; reset_password_code?: string | null; + reset_password_token?: string | null; } diff --git a/src/domain/interfaces/repositories/user-repository.ts b/src/domain/interfaces/repositories/user-repository.ts index d3ad383..de1b617 100644 --- a/src/domain/interfaces/repositories/user-repository.ts +++ b/src/domain/interfaces/repositories/user-repository.ts @@ -14,6 +14,7 @@ export interface UserRepository { generateValidationToken(user: UserRequestModel): string; verifyValidationToken(confirmation_token: string): DecodedToken | null; generateResetPasswordToken(user: UserRequestModel): string; + verifyResetPasswordToken(reset_password_token: string): DecodedToken | null; setResetPasswordCode(user: UserUpdateModel): Promise; toPublicUser(createdUser: UserResponseModel): UserResponseModel; } \ No newline at end of file diff --git a/src/domain/interfaces/use-cases/auth/reset-password.ts b/src/domain/interfaces/use-cases/auth/reset-password.ts new file mode 100644 index 0000000..562f10d --- /dev/null +++ b/src/domain/interfaces/use-cases/auth/reset-password.ts @@ -0,0 +1,4 @@ +import { ChangeCredentialsModel } from "../../../entities/auth"; +export interface ResetPasswordUseCase { + execute(credentials: ChangeCredentialsModel): Promise; +} diff --git a/src/domain/repositories/user-repository.ts b/src/domain/repositories/user-repository.ts index 74a8c2b..eeef75c 100644 --- a/src/domain/repositories/user-repository.ts +++ b/src/domain/repositories/user-repository.ts @@ -111,11 +111,20 @@ export class UserRepositoryImpl implements UserRepository { return false; } } - // TODO IMPROVE ERROR HANDLING + verifyValidationToken(confirmation_token: string): DecodedToken | null { + return this.verifyToken(confirmation_token, this.VALIDATION_TOKEN_SECRET) + } + + verifyResetPasswordToken(reset_password_token: string): DecodedToken | null { + return this.verifyToken(reset_password_token, this.RESET_PASSWORD_TOKEN_SECRET) + } + + // TODO IMPROVE ERROR HANDLING + verifyToken(token: string, secret: string): DecodedToken | null { try { - // Verify the token using the refresh secret key - const decoded = this.userJwt.verify(confirmation_token, this.VALIDATION_TOKEN_SECRET) + // Verify the token + const decoded = this.userJwt.verify(token, secret) // Attach the decoded token to the request object const decoded_token = (decoded as DecodedToken); @@ -124,7 +133,7 @@ export class UserRepositoryImpl implements UserRepository { } catch (error) { // An error occurred while fetching or comparing, log the error and return null console.log(error); - console.log(" Validation token invalid or expired."); + console.log("Token invalid or expired."); return null; } } diff --git a/src/domain/use-cases/auth/change-password.ts b/src/domain/use-cases/auth/change-password.ts index 770b10f..bf56cd7 100644 --- a/src/domain/use-cases/auth/change-password.ts +++ b/src/domain/use-cases/auth/change-password.ts @@ -7,12 +7,11 @@ export class ChangePassword implements ChangePasswordUseCase { constructor(userRepository: UserRepository) { this.userRepository = userRepository - } + async execute(current_user: DecodedToken, credentials: ChangeCredentialsModel): Promise { let nb_of_updated_user: number = 0 - // admin can update anyone password without old password if (await this.userRepository.isAdmin(current_user.user_id)) { nb_of_updated_user = await this.userRepository.changePassword(credentials) @@ -31,6 +30,5 @@ export class ChangePassword implements ChangePasswordUseCase { } else { throw new Error("Logged user cannot update this property or user"); } - } } \ No newline at end of file diff --git a/src/domain/use-cases/auth/reset-password.ts b/src/domain/use-cases/auth/reset-password.ts new file mode 100644 index 0000000..5c54408 --- /dev/null +++ b/src/domain/use-cases/auth/reset-password.ts @@ -0,0 +1,39 @@ +import { UserRepository } from "../../interfaces/repositories/user-repository"; +import { ResetPasswordUseCase } from "../../interfaces/use-cases/auth/reset-password"; +import { ChangeCredentialsModel, DecodedToken } from "../../entities/auth"; +import { UserResponseModel } from "../../entities/user"; + +export class ResetPassword implements ResetPasswordUseCase { + userRepository: UserRepository + + constructor(userRepository: UserRepository) { + this.userRepository = userRepository + } + async execute(credentials: ChangeCredentialsModel): Promise { + let nb_of_updated_user: number = 0 + let decoded_token: DecodedToken | null = null + + //is the user reset_password_token valid ? + if (credentials.reset_password_token) { + decoded_token = this.userRepository.verifyResetPasswordToken(credentials.reset_password_token) + if (!decoded_token) throw new Error("Token is not valid"); + } else throw new Error("Token is not valid"); + + // does the user bind to reset_password_code exist ? + const preexistant_user: UserResponseModel | null = await this.userRepository.getUser( + { + user_id: decoded_token.user_id, + reset_password_code: decoded_token.reset_password_code + } + ) + // if the user does not exist or the reset_password_code is not valid + if (!preexistant_user) throw new Error("User does not exist or token is not valid"); + + // is the user validated ? + if (!preexistant_user.valid_email) throw new Error("User email is not validated"); + + // change the password + nb_of_updated_user = await this.userRepository.changePassword({ ...preexistant_user, ...credentials }) + if (nb_of_updated_user == 0) throw new Error("Can't change password"); + } +} \ No newline at end of file diff --git a/src/infra/mailer/templates/reset_password_email.html b/src/infra/mailer/templates/reset_password_email.html index b96fbe5..90bca63 100644 --- a/src/infra/mailer/templates/reset_password_email.html +++ b/src/infra/mailer/templates/reset_password_email.html @@ -42,8 +42,8 @@

EcoPart app

But don’t worry! You can use the the button below to reset your password:

Validate - Account + style="display: inline-block; padding: 10px 20px; background-color: #1976D2; color: #ffffff; text-decoration: none; border-radius: 5px;">Reset + password

If the button above does not work, you can copy and paste the following link into your browser:

{{reset_password_path}}

diff --git a/src/main.ts b/src/main.ts index 4383e23..6d3bc95 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,7 @@ import { RefreshToken } from './domain/use-cases/auth/refresh-token' import { ChangePassword } from './domain/use-cases/auth/change-password' import { ValidUser } from './domain/use-cases/user/valid-user' import { ResetPasswordRequest } from './domain/use-cases/auth/reset-password-request' +import { ResetPassword } from './domain/use-cases/auth/reset-password' import { UserRepositoryImpl } from './domain/repositories/user-repository' import { AuthRepositoryImpl } from './domain/repositories/auth-repository' @@ -93,6 +94,7 @@ async function getSQLiteDS() { new RefreshToken(new UserRepositoryImpl(dataSource, bcryptAdapter, jwtAdapter, config.VALIDATION_TOKEN_SECRET, config.RESET_PASSWORD_TOKEN_SECRET), new AuthRepositoryImpl(jwtAdapter, config.ACCESS_TOKEN_SECRET, config.REFRESH_TOKEN_SECRET)), new ChangePassword(new UserRepositoryImpl(dataSource, bcryptAdapter, jwtAdapter, config.VALIDATION_TOKEN_SECRET, config.RESET_PASSWORD_TOKEN_SECRET)), new ResetPasswordRequest(new UserRepositoryImpl(dataSource, bcryptAdapter, jwtAdapter, config.VALIDATION_TOKEN_SECRET, config.RESET_PASSWORD_TOKEN_SECRET), transporter, mailerAdapter), + new ResetPassword(new UserRepositoryImpl(dataSource, bcryptAdapter, jwtAdapter, config.VALIDATION_TOKEN_SECRET, config.RESET_PASSWORD_TOKEN_SECRET)), ) server.use("/users", userMiddleWare) diff --git a/src/presentation/routers/auth-router.ts b/src/presentation/routers/auth-router.ts index bbfccb9..3e99235 100644 --- a/src/presentation/routers/auth-router.ts +++ b/src/presentation/routers/auth-router.ts @@ -9,6 +9,7 @@ import { LoginUserUseCase } from '../../domain/interfaces/use-cases/auth/login' import { RefreshTokenUseCase } from '../../domain/interfaces/use-cases/auth/refresh-token' import { ChangePasswordUseCase } from '../../domain/interfaces/use-cases/auth/change-password' import { ResetPasswordRequestUseCase } from '../../domain/interfaces/use-cases/auth/reset-password-request' +import { ResetPasswordUseCase } from '../../domain/interfaces/use-cases/auth/reset-password' // password securituy rules //HTTPS //SALTING before hashing //rate limiting //timeout //SSO export default function AuthRouter( @@ -18,8 +19,7 @@ export default function AuthRouter( refreshTokenUseCase: RefreshTokenUseCase, changePasswordUseCase: ChangePasswordUseCase, resetPasswordRequestUseCase: ResetPasswordRequestUseCase, - - + resetPasswordUseCase: ResetPasswordUseCase ) { const router = express.Router() @@ -70,7 +70,6 @@ export default function AuthRouter( } }) - router.post('/logout', async (req: Request, res: Response) => { try { res @@ -119,19 +118,22 @@ export default function AuthRouter( } }) - // // reset password confirm - // router.put('/password/reset', async (req: Request, res: Response) => { - // try { - // const token = await resetPasswordUseCase.execute(req.body) - // res.statusCode = 200// to check - // res.json(token) - // } catch (err) { - // console.log(err) - // if (err.message === "") res.status(500).send({ errors: [""] }) - // else res.status(500).send({ errors: ["Can't reset password"] }) - // } - //}) - + // reset password confirm + router.put('/password/reset', async (req: Request, res: Response) => { + try { + console.log("req.body", req.body) + await resetPasswordUseCase.execute(req.body) + res + .status(200) + .json({ response: "Password sucessfully reset, please login" }); + } catch (err) { + console.log(err) + if (err.message === "Token is not valid") res.status(401).send({ errors: ["Can't change password"] }) + if (err.message === "User does not exist or token is not valid") res.status(404).send({ errors: ["Can't change password"] }) + if (err.message === "User email is not validated") res.status(403).send({ errors: ["Can't change password"] }) + else res.status(500).send({ errors: ["Can't reset password"] }) + } + }) return router } \ No newline at end of file