← full-stack-fastapi-template  /  frontend/src/components/UserSettings/ChangePassword.tsx

1
import { zodResolver } from "@hookform/resolvers/zod"
2
import { useMutation } from "@tanstack/react-query"
3
import { useForm } from "react-hook-form"
4
import { z } from "zod"
5
6
import { type UpdatePassword, UsersService } from "@/client"
7
import {
8
  Form,
9
  FormControl,
10
  FormField,
11
  FormItem,
12
  FormLabel,
13
  FormMessage,
14
} from "@/components/ui/form"
15
import { LoadingButton } from "@/components/ui/loading-button"
16
import { PasswordInput } from "@/components/ui/password-input"
17
import useCustomToast from "@/hooks/useCustomToast"
18
import { handleError } from "@/utils"
19
20
const formSchema = z
21
  .object({
22
    current_password: z
23
      .string()
24
      .min(1, { message: "Password is required" })
25
      .min(8, { message: "Password must be at least 8 characters" }),
26
    new_password: z
27
      .string()
28
      .min(1, { message: "Password is required" })
29
      .min(8, { message: "Password must be at least 8 characters" }),
30
    confirm_password: z
31
      .string()
32
      .min(1, { message: "Password confirmation is required" }),
33
  })
34
  .refine((data) => data.new_password === data.confirm_password, {
35
    message: "The passwords don't match",
36
    path: ["confirm_password"],
37
  })
38
39
type FormData = z.infer<typeof formSchema>
40
41
const ChangePassword = () => {
42
  const { showSuccessToast, showErrorToast } = useCustomToast()
43
  const form = useForm<FormData>({
44
    resolver: zodResolver(formSchema),
45
    mode: "onSubmit",
46
    criteriaMode: "all",
47
    defaultValues: {
48
      current_password: "",
49
      new_password: "",
50
      confirm_password: "",
51
    },
52
  })
53
54
  const mutation = useMutation({
55
    mutationFn: (data: UpdatePassword) =>
56
      UsersService.updatePasswordMe({ requestBody: data }),
57
    onSuccess: () => {
58
      showSuccessToast("Password updated successfully")
59
      form.reset()
60
    },
61
    onError: handleError.bind(showErrorToast),
62
  })
63
64
  const onSubmit = async (data: FormData) => {
65
    mutation.mutate(data)
66
  }
67
68
  return (
69
    <div className="max-w-md">
70
      <h3 className="text-lg font-semibold py-4">Change Password</h3>
71
      <Form {...form}>
72
        <form
73
          onSubmit={form.handleSubmit(onSubmit)}
74
          className="flex flex-col gap-4"
75
        >
76
          <FormField
77
            control={form.control}
78
            name="current_password"
79
            render={({ field, fieldState }) => (
80
              <FormItem>
81
                <FormLabel>Current Password</FormLabel>
82
                <FormControl>
83
                  <PasswordInput
84
                    data-testid="current-password-input"
85
                    placeholder="••••••••"
86
                    aria-invalid={fieldState.invalid}
87
                    {...field}
88
                  />
89
                </FormControl>
90
                <FormMessage />
91
              </FormItem>
92
            )}
93
          />
94
95
          <FormField
96
            control={form.control}
97
            name="new_password"
98
            render={({ field, fieldState }) => (
99
              <FormItem>
100
                <FormLabel>New Password</FormLabel>
101
                <FormControl>
102
                  <PasswordInput
103
                    data-testid="new-password-input"
104
                    placeholder="••••••••"
105
                    aria-invalid={fieldState.invalid}
106
                    {...field}
107
                  />
108
                </FormControl>
109
                <FormMessage />
110
              </FormItem>
111
            )}
112
          />
113
114
          <FormField
115
            control={form.control}
116
            name="confirm_password"
117
            render={({ field, fieldState }) => (
118
              <FormItem>
119
                <FormLabel>Confirm Password</FormLabel>
120
                <FormControl>
121
                  <PasswordInput
122
                    data-testid="confirm-password-input"
123
                    placeholder="••••••••"
124
                    aria-invalid={fieldState.invalid}
125
                    {...field}
126
                  />
127
                </FormControl>
128
                <FormMessage />
129
              </FormItem>
130
            )}
131
          />
132
133
          <LoadingButton
134
            type="submit"
135
            loading={mutation.isPending}
136
            className="self-start"
137
          >
138
            Update Password
139
          </LoadingButton>
140
        </form>
141
      </Form>
142
    </div>
143
  )
144
}
145
146
export default ChangePassword
147