← full-stack-fastapi-template  /  frontend/src/routes/reset-password.tsx

1
import { zodResolver } from "@hookform/resolvers/zod"
2
import { useMutation } from "@tanstack/react-query"
3
import {
4
  createFileRoute,
5
  Link as RouterLink,
6
  redirect,
7
  useNavigate,
8
} from "@tanstack/react-router"
9
import { useForm } from "react-hook-form"
10
import { z } from "zod"
11
12
import { LoginService } from "@/client"
13
import { AuthLayout } from "@/components/Common/AuthLayout"
14
import {
15
  Form,
16
  FormControl,
17
  FormField,
18
  FormItem,
19
  FormLabel,
20
  FormMessage,
21
} from "@/components/ui/form"
22
import { LoadingButton } from "@/components/ui/loading-button"
23
import { PasswordInput } from "@/components/ui/password-input"
24
import { isLoggedIn } from "@/hooks/useAuth"
25
import useCustomToast from "@/hooks/useCustomToast"
26
import { handleError } from "@/utils"
27
28
const searchSchema = z.object({
29
  token: z.string().catch(""),
30
})
31
32
const formSchema = z
33
  .object({
34
    new_password: z
35
      .string()
36
      .min(1, { message: "Password is required" })
37
      .min(8, { message: "Password must be at least 8 characters" }),
38
    confirm_password: z
39
      .string()
40
      .min(1, { message: "Password confirmation is required" }),
41
  })
42
  .refine((data) => data.new_password === data.confirm_password, {
43
    message: "The passwords don't match",
44
    path: ["confirm_password"],
45
  })
46
47
type FormData = z.infer<typeof formSchema>
48
49
export const Route = createFileRoute("/reset-password")({
50
  component: ResetPassword,
51
  validateSearch: searchSchema,
52
  beforeLoad: async ({ search }) => {
53
    if (isLoggedIn()) {
54
      throw redirect({ to: "/" })
55
    }
56
    if (!search.token) {
57
      throw redirect({ to: "/login" })
58
    }
59
  },
60
  head: () => ({
61
    meta: [
62
      {
63
        title: "Reset Password - FastAPI Template",
64
      },
65
    ],
66
  }),
67
})
68
69
function ResetPassword() {
70
  const { token } = Route.useSearch()
71
  const { showSuccessToast, showErrorToast } = useCustomToast()
72
  const navigate = useNavigate()
73
74
  const form = useForm<FormData>({
75
    resolver: zodResolver(formSchema),
76
    mode: "onBlur",
77
    criteriaMode: "all",
78
    defaultValues: {
79
      new_password: "",
80
      confirm_password: "",
81
    },
82
  })
83
84
  const mutation = useMutation({
85
    mutationFn: (data: { new_password: string; token: string }) =>
86
      LoginService.resetPassword({ requestBody: data }),
87
    onSuccess: () => {
88
      showSuccessToast("Password updated successfully")
89
      form.reset()
90
      navigate({ to: "/login" })
91
    },
92
    onError: handleError.bind(showErrorToast),
93
  })
94
95
  const onSubmit = (data: FormData) => {
96
    mutation.mutate({ new_password: data.new_password, token })
97
  }
98
99
  return (
100
    <AuthLayout>
101
      <Form {...form}>
102
        <form
103
          onSubmit={form.handleSubmit(onSubmit)}
104
          className="flex flex-col gap-6"
105
        >
106
          <div className="flex flex-col items-center gap-2 text-center">
107
            <h1 className="text-2xl font-bold">Reset Password</h1>
108
          </div>
109
110
          <div className="grid gap-4">
111
            <FormField
112
              control={form.control}
113
              name="new_password"
114
              render={({ field }) => (
115
                <FormItem>
116
                  <FormLabel>New Password</FormLabel>
117
                  <FormControl>
118
                    <PasswordInput
119
                      data-testid="new-password-input"
120
                      placeholder="New Password"
121
                      {...field}
122
                    />
123
                  </FormControl>
124
                  <FormMessage />
125
                </FormItem>
126
              )}
127
            />
128
129
            <FormField
130
              control={form.control}
131
              name="confirm_password"
132
              render={({ field }) => (
133
                <FormItem>
134
                  <FormLabel>Confirm Password</FormLabel>
135
                  <FormControl>
136
                    <PasswordInput
137
                      data-testid="confirm-password-input"
138
                      placeholder="Confirm Password"
139
                      {...field}
140
                    />
141
                  </FormControl>
142
                  <FormMessage />
143
                </FormItem>
144
              )}
145
            />
146
147
            <LoadingButton
148
              type="submit"
149
              className="w-full"
150
              loading={mutation.isPending}
151
            >
152
              Reset Password
153
            </LoadingButton>
154
          </div>
155
156
          <div className="text-center text-sm">
157
            Remember your password?{" "}
158
            <RouterLink to="/login" className="underline underline-offset-4">
159
              Log in
160
            </RouterLink>
161
          </div>
162
        </form>
163
      </Form>
164
    </AuthLayout>
165
  )
166
}
167