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 (
+ <>
+
+
+ >
+ )
+}
+
+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/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/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
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": {}