Skip to content

Commit c147a95

Browse files
authored
✨ Add Sign Up and make OPEN_USER_REGISTRATION=True by default (#1265)
1 parent 7a9920a commit c147a95

5 files changed

Lines changed: 213 additions & 8 deletions

File tree

.env

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,h
1313
SECRET_KEY=changethis
1414
FIRST_SUPERUSER=admin@example.com
1515
FIRST_SUPERUSER_PASSWORD=changethis
16-
USERS_OPEN_REGISTRATION=False
16+
USERS_OPEN_REGISTRATION=True
1717

1818
# Emails
1919
SMTP_HOST=

frontend/src/hooks/useAuth.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useMutation, useQuery } from "@tanstack/react-query"
1+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
22
import { useNavigate } from "@tanstack/react-router"
33
import { useState } from "react"
44

@@ -8,8 +8,10 @@ import {
88
type ApiError,
99
LoginService,
1010
type UserPublic,
11+
type UserRegister,
1112
UsersService,
1213
} from "../client"
14+
import useCustomToast from "./useCustomToast"
1315

1416
const isLoggedIn = () => {
1517
return localStorage.getItem("access_token") !== null
@@ -18,12 +20,36 @@ const isLoggedIn = () => {
1820
const useAuth = () => {
1921
const [error, setError] = useState<string | null>(null)
2022
const navigate = useNavigate()
23+
const showToast = useCustomToast()
24+
const queryClient = useQueryClient()
2125
const { data: user, isLoading } = useQuery<UserPublic | null, Error>({
2226
queryKey: ["currentUser"],
2327
queryFn: UsersService.readUserMe,
2428
enabled: isLoggedIn(),
2529
})
2630

31+
const signUpMutation = useMutation({
32+
mutationFn: (data: UserRegister) =>
33+
UsersService.registerUser({ requestBody: data }),
34+
35+
onSuccess: () => {
36+
navigate({ to: "/login" })
37+
showToast("Success!", "User created successfully.", "success")
38+
},
39+
onError: (err: ApiError) => {
40+
let errDetail = (err.body as any)?.detail
41+
42+
if (err instanceof AxiosError) {
43+
errDetail = err.message
44+
}
45+
46+
showToast("Something went wrong.", `${errDetail}`, "error")
47+
},
48+
onSettled: () => {
49+
queryClient.invalidateQueries({ queryKey: ["users"] })
50+
},
51+
})
52+
2753
const login = async (data: AccessToken) => {
2854
const response = await LoginService.loginAccessToken({
2955
formData: data,
@@ -57,6 +83,7 @@ const useAuth = () => {
5783
}
5884

5985
return {
86+
signUpMutation,
6087
loginMutation,
6188
logout,
6289
user,

frontend/src/routeTree.gen.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// Import Routes
1212

1313
import { Route as rootRoute } from './routes/__root'
14+
import { Route as SignupImport } from './routes/signup'
1415
import { Route as ResetPasswordImport } from './routes/reset-password'
1516
import { Route as RecoverPasswordImport } from './routes/recover-password'
1617
import { Route as LoginImport } from './routes/login'
@@ -22,6 +23,11 @@ import { Route as LayoutAdminImport } from './routes/_layout/admin'
2223

2324
// Create/Update Routes
2425

26+
const SignupRoute = SignupImport.update({
27+
path: '/signup',
28+
getParentRoute: () => rootRoute,
29+
} as any)
30+
2531
const ResetPasswordRoute = ResetPasswordImport.update({
2632
path: '/reset-password',
2733
getParentRoute: () => rootRoute,
@@ -82,6 +88,10 @@ declare module '@tanstack/react-router' {
8288
preLoaderRoute: typeof ResetPasswordImport
8389
parentRoute: typeof rootRoute
8490
}
91+
'/signup': {
92+
preLoaderRoute: typeof SignupImport
93+
parentRoute: typeof rootRoute
94+
}
8595
'/_layout/admin': {
8696
preLoaderRoute: typeof LayoutAdminImport
8797
parentRoute: typeof LayoutImport
@@ -113,6 +123,7 @@ export const routeTree = rootRoute.addChildren([
113123
LoginRoute,
114124
RecoverPasswordRoute,
115125
ResetPasswordRoute,
126+
SignupRoute,
116127
])
117128

118129
/* prettier-ignore-end */

frontend/src/routes/login.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons"
22
import {
33
Button,
4-
Center,
54
Container,
65
FormControl,
76
FormErrorMessage,
@@ -11,6 +10,7 @@ import {
1110
InputGroup,
1211
InputRightElement,
1312
Link,
13+
Text,
1414
useBoolean,
1515
} from "@chakra-ui/react"
1616
import {
@@ -126,14 +126,18 @@ function Login() {
126126
</InputGroup>
127127
{error && <FormErrorMessage>{error}</FormErrorMessage>}
128128
</FormControl>
129-
<Center>
130-
<Link as={RouterLink} to="/recover-password" color="blue.500">
131-
Forgot password?
132-
</Link>
133-
</Center>
129+
<Link as={RouterLink} to="/recover-password" color="blue.500">
130+
Forgot password?
131+
</Link>
134132
<Button variant="primary" type="submit" isLoading={isSubmitting}>
135133
Log In
136134
</Button>
135+
<Text>
136+
Don't have an account?{" "}
137+
<Link as={RouterLink} to="/signup" color="blue.500">
138+
Sign up
139+
</Link>
140+
</Text>
137141
</Container>
138142
</>
139143
)

frontend/src/routes/signup.tsx

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import {
2+
Button,
3+
Container,
4+
Flex,
5+
FormControl,
6+
FormErrorMessage,
7+
FormLabel,
8+
Image,
9+
Input,
10+
Link,
11+
Text,
12+
} from "@chakra-ui/react"
13+
import {
14+
Link as RouterLink,
15+
createFileRoute,
16+
redirect,
17+
} from "@tanstack/react-router"
18+
import { type SubmitHandler, useForm } from "react-hook-form"
19+
20+
import Logo from "/assets/images/fastapi-logo.svg"
21+
import type { UserRegister } from "../client"
22+
import useAuth, { isLoggedIn } from "../hooks/useAuth"
23+
import { confirmPasswordRules, emailPattern, passwordRules } from "../utils"
24+
25+
export const Route = createFileRoute("/signup")({
26+
component: SignUp,
27+
beforeLoad: async () => {
28+
if (isLoggedIn()) {
29+
throw redirect({
30+
to: "/",
31+
})
32+
}
33+
},
34+
})
35+
36+
interface UserRegisterForm extends UserRegister {
37+
confirm_password: string
38+
}
39+
40+
function SignUp() {
41+
const { signUpMutation } = useAuth()
42+
const {
43+
register,
44+
handleSubmit,
45+
getValues,
46+
formState: { errors, isSubmitting },
47+
} = useForm<UserRegisterForm>({
48+
mode: "onBlur",
49+
criteriaMode: "all",
50+
defaultValues: {
51+
email: "",
52+
full_name: "",
53+
password: "",
54+
confirm_password: "",
55+
},
56+
})
57+
58+
const onSubmit: SubmitHandler<UserRegisterForm> = (data) => {
59+
signUpMutation.mutate(data)
60+
}
61+
62+
return (
63+
<>
64+
<Flex flexDir={{ base: "column", md: "row" }} justify="center" h="100vh">
65+
<Container
66+
as="form"
67+
onSubmit={handleSubmit(onSubmit)}
68+
h="100vh"
69+
maxW="sm"
70+
alignItems="stretch"
71+
justifyContent="center"
72+
gap={4}
73+
centerContent
74+
>
75+
<Image
76+
src={Logo}
77+
alt="FastAPI logo"
78+
height="auto"
79+
maxW="2xs"
80+
alignSelf="center"
81+
mb={4}
82+
/>
83+
<FormControl id="full_name" isInvalid={!!errors.full_name}>
84+
<FormLabel htmlFor="full_name" srOnly>
85+
Full Name
86+
</FormLabel>
87+
<Input
88+
id="full_name"
89+
minLength={3}
90+
{...register("full_name")}
91+
placeholder="Full Name"
92+
type="text"
93+
/>
94+
{errors.full_name && (
95+
<FormErrorMessage>{errors.full_name.message}</FormErrorMessage>
96+
)}
97+
</FormControl>
98+
<FormControl id="email" isInvalid={!!errors.email}>
99+
<FormLabel htmlFor="username" srOnly>
100+
Email
101+
</FormLabel>
102+
<Input
103+
id="email"
104+
{...register("email", {
105+
pattern: emailPattern,
106+
})}
107+
placeholder="Email"
108+
type="email"
109+
/>
110+
{errors.email && (
111+
<FormErrorMessage>{errors.email.message}</FormErrorMessage>
112+
)}
113+
</FormControl>
114+
<FormControl id="password" isInvalid={!!errors.password}>
115+
<FormLabel htmlFor="password" srOnly>
116+
Password
117+
</FormLabel>
118+
<Input
119+
id="password"
120+
{...register("password", passwordRules())}
121+
placeholder="Password"
122+
type="password"
123+
/>
124+
{errors.password && (
125+
<FormErrorMessage>{errors.password.message}</FormErrorMessage>
126+
)}
127+
</FormControl>
128+
<FormControl
129+
id="confirm_password"
130+
isInvalid={!!errors.confirm_password}
131+
>
132+
<FormLabel htmlFor="confirm_password" srOnly>
133+
Confirm Password
134+
</FormLabel>
135+
136+
<Input
137+
id="confirm_password"
138+
{...register("confirm_password", confirmPasswordRules(getValues))}
139+
placeholder="Repeat Password"
140+
type="password"
141+
/>
142+
{errors.confirm_password && (
143+
<FormErrorMessage>
144+
{errors.confirm_password.message}
145+
</FormErrorMessage>
146+
)}
147+
</FormControl>
148+
<Button variant="primary" type="submit" isLoading={isSubmitting}>
149+
Sign Up
150+
</Button>
151+
<Text>
152+
Already have an account?{" "}
153+
<Link as={RouterLink} to="/login" color="blue.500">
154+
Log In
155+
</Link>
156+
</Text>
157+
</Container>
158+
</Flex>
159+
</>
160+
)
161+
}
162+
163+
export default SignUp

0 commit comments

Comments
 (0)