Skip to content

Commit c624ea0

Browse files
committed
Add reset password section to profile page
1 parent 2f8fe5d commit c624ea0

File tree

4 files changed

+181
-53
lines changed

4 files changed

+181
-53
lines changed

backend/user/controller/user-controller.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,47 @@ export async function resetPasswordUsingCode(req, res) {
380380
}
381381
}
382382

383+
export async function resetPasswordFromProfile(req, res) {
384+
try {
385+
const { password } = req.body;
386+
const userId = req.params.id;
387+
console.log(req.body);
388+
if (password) {
389+
const user = await _findUserById(userId);
390+
if (!user) {
391+
console.log("ERROR");
392+
return res
393+
.status(404)
394+
.json({ message: `User not found with user ID ${code}` });
395+
}
396+
397+
const salt = bcrypt.genSaltSync(10);
398+
const hashedPassword = bcrypt.hashSync(password, salt);
399+
400+
const updatedUser = await _updateUserById(
401+
user.id,
402+
user.username,
403+
user.email,
404+
hashedPassword,
405+
user.bio,
406+
user.linkedin,
407+
user.github
408+
);
409+
return res.status(200).json({
410+
message: `Reset password for user ${user.username}`,
411+
data: formatUserResponse(updatedUser),
412+
});
413+
} else {
414+
return res.status(400).json({ message: "password is missing!" });
415+
}
416+
} catch (err) {
417+
console.error(err);
418+
return res
419+
.status(500)
420+
.json({ message: "Unknown error when resetting password!" });
421+
}
422+
}
423+
383424
export function formatUserResponse(user) {
384425
return {
385426
id: user.id,

backend/user/routes/user-routes.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
updateUserPrivilege,
1313
getFileUrl,
1414
verifyUser,
15+
resetPasswordFromProfile,
1516
} from "../controller/user-controller.js";
1617
import {
1718
verifyAccessToken,
@@ -32,6 +33,8 @@ router.patch(
3233

3334
router.get("/verify", verifyUser);
3435

36+
router.post("/:id/reset-password-from-profile", resetPasswordFromProfile);
37+
3538
router.post("/request-password-reset", requestPasswordReset);
3639

3740
router.post("/check-password-reset-code", checkPasswordResetCode);

frontend/src/api/user.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,38 @@ export const getUser = async (userId = "") => {
185185
return data;
186186
};
187187

188+
export const resetPasswordFromProfile = async (newPassword: string) => {
189+
const userId = getUserId();
190+
const token = getToken();
191+
const url = `${NEXT_PUBLIC_IAM_USER_SERVICE}/${userId}/reset-password-from-profile`;
192+
const response = await fetch(url, {
193+
method: "POST",
194+
headers: {
195+
"Content-Type": "application/json",
196+
Authorization: `Bearer ${token}`,
197+
},
198+
body: JSON.stringify({ password: newPassword }),
199+
});
200+
201+
const data = await response.json();
202+
if (response.status === 400) {
203+
ToastComponent.fire({
204+
icon: "error",
205+
title: data.message,
206+
});
207+
return false;
208+
} else if (response.status === 200) {
209+
ToastComponent.fire({
210+
icon: "success",
211+
title: data.message,
212+
});
213+
return true;
214+
}
215+
};
216+
188217
export const updateUser = async (userData: {
189218
username?: string;
190219
email?: string;
191-
password?: string;
192220
bio?: string;
193221
linkedin?: string;
194222
github?: string;

frontend/src/app/(user)/user/me/page.tsx

Lines changed: 108 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
updateUser,
2020
getFileUrl,
2121
ToastComponent,
22+
resetPasswordFromProfile,
2223
} from "@/api/user";
2324
import { useEffect, useRef, useState } from "react";
2425
import { User } from "@/types/user";
@@ -29,7 +30,7 @@ import { CgProfile } from "react-icons/cg";
2930
import MoonLoader from "react-spinners/MoonLoader";
3031
import { IoCloseCircle } from "react-icons/io5";
3132

32-
const formSchema = z.object({
33+
const profileFormSchema = z.object({
3334
username: z
3435
.string()
3536
.min(5, "Username must be at least 5 characters")
@@ -42,10 +43,6 @@ const formSchema = z.object({
4243
message: "Invalid URL",
4344
}),
4445
email: z.string().email("Invalid email").optional(),
45-
password: z
46-
.string()
47-
.max(100, "Password must be at most 100 characters")
48-
.optional(),
4946
bio: z.string().optional(),
5047
linkedin: z
5148
.string()
@@ -61,50 +58,78 @@ const formSchema = z.object({
6158
.optional(),
6259
});
6360

61+
const resetPasswordFormSchema = z
62+
.object({
63+
password: z
64+
.string()
65+
.min(8, "Password must be at least 8 characters")
66+
.max(100, "Password must be at most 100 characters")
67+
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
68+
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
69+
.regex(/\d/, "Password must contain at least one number")
70+
.regex(
71+
/[@$!%*?&]/,
72+
"Password must contain at least one special character (@, $, !, %, *, ?, &)"
73+
),
74+
confirmPassword: z
75+
.string()
76+
.min(8, "Password must be at least 8 characters"),
77+
})
78+
.refine((data) => data.password === data.confirmPassword, {
79+
message: "Passwords do not match",
80+
path: ["confirmPassword"], // Error message will show up on confirmPassword
81+
});
82+
6483
const ProfilePage = () => {
6584
const [token, setToken] = useState(false);
6685
const [user, setUser] = useState<User>({});
6786
const fileInputRef = useRef<HTMLInputElement | null>(null);
6887
const [isLoading, setIsLoading] = useState(false);
6988

70-
const form = useForm<z.infer<typeof formSchema>>({
71-
resolver: zodResolver(formSchema),
89+
const profileForm = useForm<z.infer<typeof profileFormSchema>>({
90+
resolver: zodResolver(profileFormSchema),
7291
defaultValues: {
7392
username: "",
7493
profilePictureUrl: "",
7594
email: "",
76-
password: "",
7795
bio: "",
7896
linkedin: "",
7997
github: "",
8098
},
8199
});
82100

101+
const resetPasswordForm = useForm<z.infer<typeof resetPasswordFormSchema>>({
102+
resolver: zodResolver(resetPasswordFormSchema),
103+
defaultValues: {
104+
password: "",
105+
confirmPassword: "",
106+
},
107+
});
108+
83109
useEffect(() => {
84110
setToken((_) => !!getToken());
85111
}, []);
86112

87113
useEffect(() => {
88114
getUser().then((res) => {
89115
setUser(res.data);
90-
form.reset(res.data);
116+
profileForm.reset(res.data);
91117
});
92-
}, [form]);
118+
}, [profileForm]);
93119

94-
const onSubmit = async (data: z.infer<typeof formSchema>) => {
95-
// remove unnecessary fields
96-
if (!data.password) delete data.password;
97-
if (data.password && data.password.length < 8) {
98-
Swal.fire({
99-
icon: "error",
100-
title: "Password must be at least 8 characters",
101-
});
102-
return;
103-
}
120+
const onUpdateProfileSubmit = async (
121+
data: z.infer<typeof profileFormSchema>
122+
) => {
104123
await updateUser(data);
105124
setUser(data);
106125
};
107126

127+
const onResetPasswordSubmit = async (
128+
data: z.infer<typeof resetPasswordFormSchema>
129+
) => {
130+
await resetPasswordFromProfile(data.password);
131+
};
132+
108133
const triggerFileInput = () => {
109134
if (fileInputRef.current && !isLoading) {
110135
console.log("Click");
@@ -127,7 +152,7 @@ const ProfilePage = () => {
127152
const res = await getFileUrl(userId, formData);
128153

129154
if (res.fileUrl) {
130-
form.setValue("profilePictureUrl", res.fileUrl);
155+
profileForm.setValue("profilePictureUrl", res.fileUrl);
131156
setUser({ ...user, profilePictureUrl: res.fileUrl });
132157
}
133158
}
@@ -144,13 +169,13 @@ const ProfilePage = () => {
144169
<h1 className="text-white font-extrabold text-h1">
145170
Welcome, {user?.username}!
146171
</h1>
147-
<Form {...form}>
172+
<Form {...profileForm}>
148173
<form
149174
className="my-10 grid gap-4"
150-
onSubmit={form.handleSubmit(onSubmit)}
175+
onSubmit={profileForm.handleSubmit(onUpdateProfileSubmit)}
151176
>
152177
<FormField
153-
control={form.control}
178+
control={profileForm.control}
154179
name="profilePictureUrl"
155180
render={({ field }) => (
156181
<div>
@@ -179,7 +204,7 @@ const ProfilePage = () => {
179204
size={24}
180205
onClick={(e) => {
181206
e.stopPropagation();
182-
form.setValue("profilePictureUrl", "");
207+
profileForm.setValue("profilePictureUrl", "");
183208
setUser({ ...user, profilePictureUrl: "" });
184209
}}
185210
/>
@@ -215,7 +240,7 @@ const ProfilePage = () => {
215240
/>
216241

217242
<FormField
218-
control={form.control}
243+
control={profileForm.control}
219244
name="username"
220245
render={({ field }) => (
221246
<FormItem>
@@ -235,7 +260,7 @@ const ProfilePage = () => {
235260
)}
236261
/>
237262
<FormField
238-
control={form.control}
263+
control={profileForm.control}
239264
name="email"
240265
render={({ field }) => (
241266
<FormItem>
@@ -254,29 +279,9 @@ const ProfilePage = () => {
254279
</FormItem>
255280
)}
256281
/>
282+
257283
<FormField
258-
control={form.control}
259-
name="password"
260-
render={({ field }) => (
261-
<FormItem>
262-
<FormLabel className="text-yellow-500 text-lg">
263-
NEW PASSWORD
264-
</FormLabel>
265-
<FormControl>
266-
<Input
267-
type="password"
268-
placeholder="password"
269-
{...field}
270-
className="focus:border-yellow-500 text-white"
271-
/>
272-
</FormControl>
273-
{/* <FormDescription>This is your public display name.</FormDescription> */}
274-
<FormMessage />
275-
</FormItem>
276-
)}
277-
/>
278-
<FormField
279-
control={form.control}
284+
control={profileForm.control}
280285
name="bio"
281286
render={({ field }) => (
282287
<FormItem>
@@ -294,7 +299,7 @@ const ProfilePage = () => {
294299
)}
295300
/>
296301
<FormField
297-
control={form.control}
302+
control={profileForm.control}
298303
name="linkedin"
299304
render={({ field }) => (
300305
<FormItem>
@@ -314,7 +319,7 @@ const ProfilePage = () => {
314319
)}
315320
/>
316321
<FormField
317-
control={form.control}
322+
control={profileForm.control}
318323
name="github"
319324
render={({ field }) => (
320325
<FormItem>
@@ -336,12 +341,63 @@ const ProfilePage = () => {
336341
<Button
337342
type="submit"
338343
className="bg-yellow-500 hover:bg-yellow-300 px-4 py-2 my-2 rounded-md text-black"
339-
disabled={form.formState.isSubmitting || isLoading}
344+
disabled={profileForm.formState.isSubmitting || isLoading}
340345
>
341346
Save Changes
342347
</Button>
343348
</form>
344349
</Form>
350+
351+
<Form {...resetPasswordForm}>
352+
<form
353+
className="flex flex-col gap-4"
354+
onSubmit={resetPasswordForm.handleSubmit(onResetPasswordSubmit)}
355+
>
356+
<FormField
357+
control={resetPasswordForm.control}
358+
name="password"
359+
render={({ field }) => (
360+
<FormItem>
361+
<FormLabel className="text-yellow-500 text-lg">
362+
NEW PASSWORD
363+
</FormLabel>
364+
<FormControl>
365+
<Input
366+
type="password"
367+
placeholder="new password"
368+
{...field}
369+
className="focus:border-yellow-500 text-white"
370+
/>
371+
</FormControl>
372+
<FormMessage />
373+
</FormItem>
374+
)}
375+
/>
376+
<FormField
377+
control={resetPasswordForm.control}
378+
name="confirmPassword"
379+
render={({ field }) => (
380+
<FormItem>
381+
<FormLabel className="text-yellow-500 text-lg">
382+
CONFIRM NEW PASSWORD
383+
</FormLabel>
384+
<FormControl>
385+
<Input
386+
type="password"
387+
placeholder="confirm new password"
388+
{...field}
389+
className="focus:border-yellow-500 text-white"
390+
/>
391+
</FormControl>
392+
<FormMessage />
393+
</FormItem>
394+
)}
395+
/>
396+
<Button variant="destructive" type="submit">
397+
Reset Password
398+
</Button>
399+
</form>
400+
</Form>
345401
</div>
346402
)
347403
);

0 commit comments

Comments
 (0)