Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add change password form to the profile settings #518

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -52,7 +53,7 @@ const Profile = () => {
}

return (
<div className="p-10">
<div className="p-10 flex flex-col gap-8">
<Card>
<CardHeader>
<CardTitle>Profile</CardTitle>
Expand Down Expand Up @@ -84,6 +85,7 @@ const Profile = () => {
</div>
</CardContent>
</Card>
<ChangePasswordForm />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
old_password: '',
new_password: ''
},
})

const onSubmit = (values: z.infer<typeof formSchema>) => {
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<any[]>(['users'], (oldQueryData = []) => {
return [...oldQueryData, data];
});
return (
<div className="flex flex-row items-center">
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.49991 0.877045C3.84222 0.877045 0.877075 3.84219 0.877075 7.49988C0.877075 11.1575 3.84222 14.1227 7.49991 14.1227C11.1576 14.1227 14.1227 11.1575 14.1227 7.49988C14.1227 3.84219 11.1576 0.877045 7.49991 0.877045ZM1.82708 7.49988C1.82708 4.36686 4.36689 1.82704 7.49991 1.82704C10.6329 1.82704 13.1727 4.36686 13.1727 7.49988C13.1727 10.6329 10.6329 13.1727 7.49991 13.1727C4.36689 13.1727 1.82708 10.6329 1.82708 7.49988ZM10.1589 5.53774C10.3178 5.31191 10.2636 5.00001 10.0378 4.84109C9.81194 4.68217 9.50004 4.73642 9.34112 4.96225L6.51977 8.97154L5.35681 7.78706C5.16334 7.59002 4.84677 7.58711 4.64973 7.78058C4.45268 7.97404 4.44978 8.29061 4.64325 8.48765L6.22658 10.1003C6.33054 10.2062 6.47617 10.2604 6.62407 10.2483C6.77197 10.2363 6.90686 10.1591 6.99226 10.0377L10.1589 5.53774Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
<div className="ml-2">
Password for user
<Badge variant="secondary" className="rounded-sm px-1 mx-2 font-normal">
{`${profile?.email}`}
</Badge>
has been updated successfully
</div>
</div>
);
},
error: (err: any) => err.message || 'Error'
});
};

return (
<>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card>
<CardHeader>
<CardTitle>Change Password</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<div className="grid gap-4">
<div className="grid gap-2">
<FormField
name="old_password"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Current Password</FormLabel>
<FormControl>
<PasswordInput {...field} placeholder='Current Password' />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid gap-2">
<FormField
name="new_password"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>New Password</FormLabel>
<FormControl>
<PasswordInput {...field} placeholder='New Password' autoComplete="new-password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</CardContent>
<CardFooter>
<Button type='submit' size="sm" className="h-7 gap-1" >Save</Button>
</CardFooter>
</Card>
</form>
</Form>
</>
)
}

export default ChangePasswordForm
49 changes: 49 additions & 0 deletions apps/client-ts/src/hooks/create/useChangePassword.tsx
Original file line number Diff line number Diff line change
@@ -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;
9 changes: 9 additions & 0 deletions packages/api/src/@core/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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);
}
}
80 changes: 79 additions & 1 deletion packages/api/src/@core/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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');
Expand Down
13 changes: 13 additions & 0 deletions packages/api/src/@core/auth/dto/change-password.dto.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions packages/api/src/@core/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'> {}
Expand Down
Loading
Loading