From 7b2adce0f152c20797338dd9ab25f23835880b9b Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 19 Jun 2024 20:21:05 +0300 Subject: [PATCH 1/2] Add change password form to the settings --- .../src/app/(Dashboard)/b2c/profile/page.tsx | 4 +- .../ChangePasswordForm.tsx | 141 ++++++++++++++++++ .../src/hooks/create/useChangePassword.tsx | 49 ++++++ .../src/@core/auth/dto/change-password.dto.ts | 13 ++ 4 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 apps/client-ts/src/components/Auth/CustomLoginComponent/ChangePasswordForm.tsx create mode 100644 apps/client-ts/src/hooks/create/useChangePassword.tsx create mode 100644 packages/api/src/@core/auth/dto/change-password.dto.ts diff --git a/apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx b/apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx index 6fddc0616..36103fe69 100644 --- a/apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx +++ b/apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx @@ -17,6 +17,7 @@ import useProjectStore from "@/state/projectStore" import { useQueryClient } from '@tanstack/react-query'; import { useState } from "react"; import { toast } from "sonner"; +import ChangePasswordForm from "@/components/Auth/CustomLoginComponent/ChangePasswordForm"; const Profile = () => { @@ -52,7 +53,7 @@ const Profile = () => { } return ( -
+
Profile @@ -84,6 +85,7 @@ const Profile = () => {
+
); }; diff --git a/apps/client-ts/src/components/Auth/CustomLoginComponent/ChangePasswordForm.tsx b/apps/client-ts/src/components/Auth/CustomLoginComponent/ChangePasswordForm.tsx new file mode 100644 index 000000000..47db92a18 --- /dev/null +++ b/apps/client-ts/src/components/Auth/CustomLoginComponent/ChangePasswordForm.tsx @@ -0,0 +1,141 @@ +"use client" +import React from 'react' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import * as z from "zod" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { PasswordInput } from '@/components/ui/password-input' +import { toast } from 'sonner' +import { Badge } from '@/components/ui/badge' +import { useQueryClient } from '@tanstack/react-query' +import useChangePassword from '@/hooks/create/useChangePassword' +import useProfileStore from '@/state/profileStore' + +const formSchema = z.object({ + old_password: z.string().min(2, { + message: "Enter Your Password.", + }), + new_password: z.string().min(2, { + message: "Enter New passowrd.", + }), +}) + +const ChangePasswordForm = () => { + const { changePasswordPromise } = useChangePassword(); + const { profile } = useProfileStore(); + const queryClient = useQueryClient(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + old_password: '', + new_password: '' + }, + }) + + const onSubmit = (values: z.infer) => { + if (!profile?.id_user || !profile?.email) { + throw new Error("Profile ID or email is missing."); + } + + toast.promise( + changePasswordPromise({ + id_user: profile.id_user, + email: profile.email, + old_password_hash: values.old_password, + new_password_hash: values.new_password, + }), + { + loading: 'Loading...', + success: (data: any) => { + form.reset(); + queryClient.setQueryData(['users'], (oldQueryData = []) => { + return [...oldQueryData, data]; + }); + return ( +
+ +
+ Password for user + + {`${profile?.email}`} + + has been updated successfully +
+
+ ); + }, + error: (err: any) => err.message || 'Error' + }); + }; + + return ( + <> +
+ + + + Change Password + + +
+
+ ( + + Current Password + + + + + + )} + /> +
+
+ ( + + New Password + + + + + + )} + /> +
+
+
+ + + +
+
+ + + ) +} + +export default ChangePasswordForm \ No newline at end of file diff --git a/apps/client-ts/src/hooks/create/useChangePassword.tsx b/apps/client-ts/src/hooks/create/useChangePassword.tsx new file mode 100644 index 000000000..e4c4d9ee1 --- /dev/null +++ b/apps/client-ts/src/hooks/create/useChangePassword.tsx @@ -0,0 +1,49 @@ +import config from '@/lib/config'; +import { useMutation } from '@tanstack/react-query'; + +interface IChangePasswordInputDto { + id_user: string; + email: string; + old_password_hash: string; + new_password_hash: string; +} + +const useChangePassword = () => { + const changePassword = async (newPasswordData: IChangePasswordInputDto) => { + // Fetch the token + const response = await fetch(`${config.API_URL}/auth/change-password`, { + method: 'POST', + body: JSON.stringify(newPasswordData), + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error("Changing Password Failed!!") + } + + return response.json(); + }; + + const changePasswordPromise = (data: IChangePasswordInputDto) => { + return new Promise(async (resolve, reject) => { + try { + const result = await changePassword(data); + resolve(result); + + } catch (error) { + reject(error); + } + }); + }; + + return { + mutationFn: useMutation({ + mutationFn: changePassword, + }), + changePasswordPromise + } +}; + +export default useChangePassword; diff --git a/packages/api/src/@core/auth/dto/change-password.dto.ts b/packages/api/src/@core/auth/dto/change-password.dto.ts new file mode 100644 index 000000000..d334c8ef4 --- /dev/null +++ b/packages/api/src/@core/auth/dto/change-password.dto.ts @@ -0,0 +1,13 @@ +import { IsEmail, IsString, MinLength } from 'class-validator'; + +export class ChangePasswordDto { + @IsEmail() + email: string; + + @IsString() + old_password_hash: string; + + @IsString() + @MinLength(9, { message: 'New password must be at least 9 characters long' }) + new_password_hash: string; +} \ No newline at end of file From 22de37f05e4eeeca45186175fd01db03b6d76994 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 19 Jun 2024 23:35:17 +0300 Subject: [PATCH 2/2] Add backend point for changing the password --- .../api/src/@core/auth/auth.controller.ts | 9 +++ packages/api/src/@core/auth/auth.service.ts | 80 ++++++++++++++++++- packages/api/src/@core/utils/errors.ts | 1 + packages/api/swagger/swagger-spec.json | 45 +++++++++++ 4 files changed, 134 insertions(+), 1 deletion(-) diff --git a/packages/api/src/@core/auth/auth.controller.ts b/packages/api/src/@core/auth/auth.controller.ts index a2aa1d5ce..499d03f07 100644 --- a/packages/api/src/@core/auth/auth.controller.ts +++ b/packages/api/src/@core/auth/auth.controller.ts @@ -16,6 +16,7 @@ import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiKeyDto } from './dto/api-key.dto'; import { LoginDto } from './dto/login.dto'; import { RefreshDto } from './dto/refresh.dto'; +import { ChangePasswordDto } from './dto/change-password.dto'; @ApiTags('auth') @Controller('auth') @@ -107,4 +108,12 @@ export class AuthController { last_name, ); } + + @ApiOperation({ operationId: 'changePassword', summary: 'Change password' }) + @ApiBody({ type: ChangePasswordDto }) + @ApiResponse({ status: 201 }) + @Post('change-password') + async changePassword(@Body() newPasswordRequest: ChangePasswordDto) { + return this.authService.changePassword(newPasswordRequest); + } } diff --git a/packages/api/src/@core/auth/auth.service.ts b/packages/api/src/@core/auth/auth.service.ts index bb9ae6b85..bb2e332ed 100644 --- a/packages/api/src/@core/auth/auth.service.ts +++ b/packages/api/src/@core/auth/auth.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { CreateUserDto } from './dto/create-user.dto'; import { PrismaService } from '../prisma/prisma.service'; @@ -10,6 +10,7 @@ import { AuthError, throwTypedError } from '@@core/utils/errors'; import { LoginDto } from './dto/login.dto'; import { VerifyUserDto } from './dto/verify-user.dto'; import { ProjectsService } from '@@core/projects/projects.service'; +import { ChangePasswordDto } from './dto/change-password.dto'; @Injectable() export class AuthService { @@ -236,6 +237,83 @@ export class AuthService { } } + async changePassword(newPasswordRequest: ChangePasswordDto) { + try { + const foundUser = await this.prisma.users.findFirst({ + where: { + email: newPasswordRequest.email, + }, + }); + + if (!foundUser) { + throw new ReferenceError('User undefined!'); + } + + const project = await this.prisma.projects.findFirst({ + where: { + id_user: foundUser.id_user, + }, + }); + + if (!project) { + throw new ReferenceError('Project undefined!'); + } + + const isEq = await bcrypt.compare( + newPasswordRequest.old_password_hash, + foundUser.password_hash, + ); + + if (!isEq) { + throw new ReferenceError( + 'Bcrypt Invalid credentials, mismatch in password.', + ); + } + + const hashedNewPassword = await bcrypt.hash(newPasswordRequest.new_password_hash, 10); + await this.prisma.users.update({ + where: { + id_user: foundUser.id_user + }, + data: { + password_hash: hashedNewPassword, + }, + }); + + const { ...userData } = foundUser; + + const payload = { + email: userData.email, + sub: userData.id_user, + first_name: userData.first_name, + last_name: userData.last_name, + id_project: project.id_project, + }; + + return { + user: { + id_user: foundUser.id_user, + email: foundUser.email, + first_name: foundUser.first_name, + last_name: foundUser.last_name, + }, + access_token: this.jwtService.sign(payload, { + secret: process.env.JWT_SECRET, + }), // token used to generate api keys + }; + } catch (error) { + throwTypedError( + new AuthError({ + name: 'CHANGE_USER_PASSWORD_ERROR', + message: 'failed to updated password', + cause: error, + }), + this.logger, + ); + } + } + + hashApiKey(apiKey: string): string { try { return crypto.createHash('sha256').update(apiKey).digest('hex'); diff --git a/packages/api/src/@core/utils/errors.ts b/packages/api/src/@core/utils/errors.ts index 9de2515e2..4f6ed6593 100644 --- a/packages/api/src/@core/utils/errors.ts +++ b/packages/api/src/@core/utils/errors.ts @@ -140,6 +140,7 @@ export class AuthError extends ErrorBase< | 'GENERATE_API_KEY_ERROR' | 'VALIDATE_API_KEY_ERROR' | 'EMAIL_ALREADY_EXISTS_ERROR' + | 'CHANGE_USER_PASSWORD_ERROR' > {} export class PassthroughRequestError extends ErrorBase<'PASSTHROUGH_REMOTE_API_CALL_ERROR'> {} diff --git a/packages/api/swagger/swagger-spec.json b/packages/api/swagger/swagger-spec.json index 9d827af6c..59b2b4291 100644 --- a/packages/api/swagger/swagger-spec.json +++ b/packages/api/swagger/swagger-spec.json @@ -239,6 +239,31 @@ ] } }, + "/auth/change-password": { + "post": { + "operationId": "changePassword", + "summary": "Change password", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChangePasswordDto" + } + } + } + }, + "responses": { + "201": { + "description": "" + } + }, + "tags": [ + "auth" + ] + } + }, "/connections/oauth/callback": { "get": { "operationId": "handleOAuthCallback", @@ -22646,6 +22671,26 @@ "projectId" ] }, + "ChangePasswordDto": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "old_password_hash": { + "type": "string" + }, + "new_password_hash": { + "type": "string", + "minLength": 9 + } + }, + "required": [ + "email", + "old_password_hash", + "new_password_hash" + ] + }, "BodyDataType": { "type": "object", "properties": {}