Skip to content

Commit

Permalink
added change password feature
Browse files Browse the repository at this point in the history
  • Loading branch information
AuenKr committed Oct 5, 2024
1 parent 01a09fd commit 2c2bc56
Show file tree
Hide file tree
Showing 8 changed files with 662 additions and 3 deletions.
357 changes: 355 additions & 2 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

90 changes: 90 additions & 0 deletions src/actions/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
'use server';
import db from '@/db';
import { InputTypeChangePassword, ReturnTypeChangePassword } from './types';
import { getServerSession } from 'next-auth';
import { authOptions, validateUser } from '@/lib/auth';
import bcrypt from 'bcrypt';
import { createSafeAction } from '@/lib/create-safe-action';
import { ChangePasswordSchema } from './schema';

export const logoutUser = async (email: string, adminPassword: string) => {
if (adminPassword !== process.env.ADMIN_SECRET) {
Expand All @@ -25,3 +31,87 @@ export const logoutUser = async (email: string, adminPassword: string) => {

return { message: 'User logged out' };
};

const changePasswordHandler = async (
data: InputTypeChangePassword,
): Promise<ReturnTypeChangePassword> => {
const session = await getServerSession(authOptions);

if (!session || !session.user.id) {
return {
error: 'Unauthorized',
};
}

const { currentpassword, confirmpassword, newpassword } = data;

if (newpassword !== confirmpassword) {
return {
error: 'New and Confirm password does not match',
};
}
const { email } = session.user;

// Get user token(require to update password)
const user = await validateUser(email, currentpassword);
if (!user.data)
return {
error: 'User not found',
};

const { userid, token } = user.data;

// End point to change password
const url = `https://harkiratapi.classx.co.in/post/changesecurity`;

const myHeaders = new Headers();
myHeaders.append('auth-key', process.env.APPX_AUTH_KEY || '');
myHeaders.append('client-service', process.env.APPX_CLIENT_SERVICE || '');
myHeaders.append('authorization', token);

const body = new FormData();
body.append('currentpassword', currentpassword);
body.append('newpassword', newpassword);
body.append('confirmpassword', confirmpassword);
body.append('userid', userid);

try {
// Changing password to Appx
const response = await fetch(url, {
method: 'POST',
headers: myHeaders,
body,
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}

// Store new password in DB
const hashedPassword = await bcrypt.hash(newpassword, 10);
await db.user.update({
where: {
email,
},
data: {
password: hashedPassword,
token
},
});

return {
data: {
message: 'Password updated',
},
};
} catch (error) {
console.error('Error updating password user:', error);
return {
error: 'Fail to update password'
};
}
};

export const changePassword = createSafeAction(
ChangePasswordSchema,
changePasswordHandler,
);
11 changes: 11 additions & 0 deletions src/actions/user/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import z from 'zod';

export const ChangePasswordSchema = z.object({
currentpassword: z.string({ message: 'Required' }),
newpassword: z
.string({ message: 'Required' })
.min(6, { message: 'Min length 6 required' }),
confirmpassword: z
.string({ message: 'Required' })
.min(6, { message: 'Min length 6 required' }),
});
14 changes: 14 additions & 0 deletions src/actions/user/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ActionState } from '@/lib/create-safe-action';
import { z } from 'zod';
import { ChangePasswordSchema } from './schema';

export type InputTypeChangePassword = z.infer<typeof ChangePasswordSchema>;

interface ReturnChangePassword {
message: string;
}

export type ReturnTypeChangePassword = ActionState<
InputTypeChangePassword,
ReturnChangePassword
>;
16 changes: 16 additions & 0 deletions src/app/password/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { PasswordChange } from '@/components/account/PasswordChange';
import { authOptions } from '@/lib/auth';
import { getServerSession } from 'next-auth';
import { redirect } from 'next/navigation';

export default async function PasswordChangePage() {
const session = await getServerSession(authOptions);
if (!session?.user) {
redirect('/');
}
return (
<section className="wrapper relative flex min-h-screen items-center justify-center overflow-hidden antialiased">
<PasswordChange />
</section>
);
}
168 changes: 168 additions & 0 deletions src/components/account/PasswordChange.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
'use client';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Input } from '../ui/input';
import { Button } from '../ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useRef, useState } from 'react';
import { changePassword } from '@/actions/user';
import { toast } from 'sonner';
import { signOut } from 'next-auth/react';
import { ChangePasswordSchema } from '@/actions/user/schema';
import { Eye, EyeOff } from 'lucide-react';
import { motion } from 'framer-motion';

export function PasswordChange() {
const [checkingPassword, setCheckingPassword] = useState<boolean>(false);
const [viewPassword, setViewPassword] = useState<boolean>(false);
const [viewNewPassword, setViewNewPassword] = useState<boolean>(false);
const [viewConfirmPassword, setViewConfirmPassword] =
useState<boolean>(false);

const isValidPass = useRef<boolean>(true);
const formSchema = ChangePasswordSchema;

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
});

async function onSubmit(values: z.infer<typeof formSchema>) {
if (values.newpassword === values.confirmpassword)
isValidPass.current = true;
else isValidPass.current = false;

if (!isValidPass.current) return;

setCheckingPassword(true);

const res = await changePassword({
currentpassword: values.currentpassword,
newpassword: values.newpassword,
confirmpassword: values.confirmpassword,
});
if (!res?.error) {
toast.success('Password Changed\nLogin Again');
await signOut();
} else {
toast.error('oops something went wrong..!');
}
setCheckingPassword(false);
}

return (
<motion.div
initial={{ y: -40, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{
duration: 0.5,
ease: 'easeInOut',
type: 'spring',
damping: 10,
}}
className="flex w-full flex-col justify-between gap-12 rounded-2xl bg-primary/5 p-8 md:max-w-[30vw]"
>
<h2 className="text-center text-xl font-bold">Change Password</h2>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="currentpassword"
render={({ field }) => (
<FormItem>
<FormLabel>Current Password</FormLabel>
<FormControl>
<div className="flex items-center justify-evenly space-x-1">
<Input
placeholder="Enter your current Password"
{...field}
type={viewPassword ? 'text' : 'password'}
/>
<span
onClick={() => setViewPassword((prev) => !prev)}
className="hover:cursor-pointer"
>
{viewPassword ? <EyeOff /> : <Eye />}
</span>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newpassword"
render={({ field }) => (
<FormItem>
<FormLabel>New Password</FormLabel>
<FormControl>
<div className="flex items-center justify-evenly space-x-1">
<Input
placeholder="Enter your new Password"
{...field}
type={viewNewPassword ? 'text' : 'password'}
/>
<span
onClick={() => setViewNewPassword((prev) => !prev)}
className="hover:cursor-pointer"
>
{viewNewPassword ? <EyeOff /> : <Eye />}
</span>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmpassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm New Password</FormLabel>
<FormControl>
<div className="flex items-center justify-evenly space-x-1">
<Input
placeholder="Confirm your current Password"
{...field}
type={viewConfirmPassword ? 'text' : 'password'}
/>
<span
onClick={() => setViewConfirmPassword((prev) => !prev)}
className="hover:cursor-pointer"
>
{viewConfirmPassword ? <EyeOff /> : <Eye />}
</span>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isValidPass.current ? null : (
<p className="text-md text-red-800">
New and Confirm password does not match
</p>
)}
<div className="flex items-center justify-center">
<Button
type="submit"
variant={'branding'}
disabled={checkingPassword as boolean}
>
Change Password
</Button>
</div>
</form>
</Form>
</motion.div>
);
}
6 changes: 6 additions & 0 deletions src/components/profile-menu/ProfileDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Bookmark,
History,
MessageSquare,
Settings,
} from 'lucide-react';
import {
DropdownMenu,
Expand Down Expand Up @@ -48,6 +49,11 @@ const ProfileDropdown = () => {
icon: <Calendar className="size-4" />,
label: 'Calendar',
},
{
href: '/password',
icon: <Settings className="size-4" />,
label: 'Change Password',
},
];

return (
Expand Down
3 changes: 2 additions & 1 deletion src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ const generateJWT = async (payload: JWTPayload) => {

return jwt;
};
async function validateUser(

export async function validateUser(
email: string,
password: string,
): Promise<
Expand Down

0 comments on commit 2c2bc56

Please sign in to comment.