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

1
import { zodResolver } from "@hookform/resolvers/zod"
2
import {
3
  createFileRoute,
4
  Link as RouterLink,
5
  redirect,
6
} from "@tanstack/react-router"
7
import { useForm } from "react-hook-form"
8
import { z } from "zod"
9
10
import type { Body_login_login_access_token as AccessToken } from "@/client"
11
import { AuthLayout } from "@/components/Common/AuthLayout"
12
import {
13
  Form,
14
  FormControl,
15
  FormField,
16
  FormItem,
17
  FormLabel,
18
  FormMessage,
19
} from "@/components/ui/form"
20
import { Input } from "@/components/ui/input"
21
import { LoadingButton } from "@/components/ui/loading-button"
22
import { PasswordInput } from "@/components/ui/password-input"
23
import useAuth, { isLoggedIn } from "@/hooks/useAuth"
24
25
const formSchema = z.object({
26
  username: z.email(),
27
  password: z
28
    .string()
29
    .min(1, { message: "Password is required" })
30
    .min(8, { message: "Password must be at least 8 characters" }),
31
}) satisfies z.ZodType<AccessToken>
32
33
type FormData = z.infer<typeof formSchema>
34
35
export const Route = createFileRoute("/login")({
36
  component: Login,
37
  beforeLoad: async () => {
38
    if (isLoggedIn()) {
39
      throw redirect({
40
        to: "/",
41
      })
42
    }
43
  },
44
  head: () => ({
45
    meta: [
46
      {
47
        title: "Log In - FastAPI Template",
48
      },
49
    ],
50
  }),
51
})
52
53
function Login() {
54
  const { loginMutation } = useAuth()
55
  const form = useForm<FormData>({
56
    resolver: zodResolver(formSchema),
57
    mode: "onBlur",
58
    criteriaMode: "all",
59
    defaultValues: {
60
      username: "",
61
      password: "",
62
    },
63
  })
64
65
  const onSubmit = (data: FormData) => {
66
    if (loginMutation.isPending) return
67
    loginMutation.mutate(data)
68
  }
69
70
  return (
71
    <AuthLayout>
72
      <Form {...form}>
73
        <form
74
          onSubmit={form.handleSubmit(onSubmit)}
75
          className="flex flex-col gap-6"
76
        >
77
          <div className="flex flex-col items-center gap-2 text-center">
78
            <h1 className="text-2xl font-bold">Login to your account</h1>
79
          </div>
80
81
          <div className="grid gap-4">
82
            <FormField
83
              control={form.control}
84
              name="username"
85
              render={({ field }) => (
86
                <FormItem>
87
                  <FormLabel>Email</FormLabel>
88
                  <FormControl>
89
                    <Input
90
                      data-testid="email-input"
91
                      placeholder="user@example.com"
92
                      type="email"
93
                      {...field}
94
                    />
95
                  </FormControl>
96
                  <FormMessage className="text-xs" />
97
                </FormItem>
98
              )}
99
            />
100
101
            <FormField
102
              control={form.control}
103
              name="password"
104
              render={({ field }) => (
105
                <FormItem>
106
                  <div className="flex items-center">
107
                    <FormLabel>Password</FormLabel>
108
                    <RouterLink
109
                      to="/recover-password"
110
                      className="ml-auto text-sm underline-offset-4 hover:underline"
111
                    >
112
                      Forgot your password?
113
                    </RouterLink>
114
                  </div>
115
                  <FormControl>
116
                    <PasswordInput
117
                      data-testid="password-input"
118
                      placeholder="Password"
119
                      {...field}
120
                    />
121
                  </FormControl>
122
                  <FormMessage className="text-xs" />
123
                </FormItem>
124
              )}
125
            />
126
127
            <LoadingButton type="submit" loading={loginMutation.isPending}>
128
              Log In
129
            </LoadingButton>
130
          </div>
131
132
          <div className="text-center text-sm">
133
            Don't have an account yet?{" "}
134
            <RouterLink to="/signup" className="underline underline-offset-4">
135
              Sign up
136
            </RouterLink>
137
          </div>
138
        </form>
139
      </Form>
140
    </AuthLayout>
141
  )
142
}
143