✨ Add Sign Up and make OPEN_USER_REGISTRATION=True
by default (#1265)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { useNavigate } from "@tanstack/react-router"
|
||||
import { useState } from "react"
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
type ApiError,
|
||||
LoginService,
|
||||
type UserPublic,
|
||||
type UserRegister,
|
||||
UsersService,
|
||||
} from "../client"
|
||||
import useCustomToast from "./useCustomToast"
|
||||
|
||||
const isLoggedIn = () => {
|
||||
return localStorage.getItem("access_token") !== null
|
||||
@@ -18,12 +20,36 @@ const isLoggedIn = () => {
|
||||
const useAuth = () => {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const navigate = useNavigate()
|
||||
const showToast = useCustomToast()
|
||||
const queryClient = useQueryClient()
|
||||
const { data: user, isLoading } = useQuery<UserPublic | null, Error>({
|
||||
queryKey: ["currentUser"],
|
||||
queryFn: UsersService.readUserMe,
|
||||
enabled: isLoggedIn(),
|
||||
})
|
||||
|
||||
const signUpMutation = useMutation({
|
||||
mutationFn: (data: UserRegister) =>
|
||||
UsersService.registerUser({ requestBody: data }),
|
||||
|
||||
onSuccess: () => {
|
||||
navigate({ to: "/login" })
|
||||
showToast("Success!", "User created successfully.", "success")
|
||||
},
|
||||
onError: (err: ApiError) => {
|
||||
let errDetail = (err.body as any)?.detail
|
||||
|
||||
if (err instanceof AxiosError) {
|
||||
errDetail = err.message
|
||||
}
|
||||
|
||||
showToast("Something went wrong.", `${errDetail}`, "error")
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["users"] })
|
||||
},
|
||||
})
|
||||
|
||||
const login = async (data: AccessToken) => {
|
||||
const response = await LoginService.loginAccessToken({
|
||||
formData: data,
|
||||
@@ -57,6 +83,7 @@ const useAuth = () => {
|
||||
}
|
||||
|
||||
return {
|
||||
signUpMutation,
|
||||
loginMutation,
|
||||
logout,
|
||||
user,
|
||||
|
@@ -11,6 +11,7 @@
|
||||
// Import Routes
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as SignupImport } from './routes/signup'
|
||||
import { Route as ResetPasswordImport } from './routes/reset-password'
|
||||
import { Route as RecoverPasswordImport } from './routes/recover-password'
|
||||
import { Route as LoginImport } from './routes/login'
|
||||
@@ -22,6 +23,11 @@ import { Route as LayoutAdminImport } from './routes/_layout/admin'
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const SignupRoute = SignupImport.update({
|
||||
path: '/signup',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const ResetPasswordRoute = ResetPasswordImport.update({
|
||||
path: '/reset-password',
|
||||
getParentRoute: () => rootRoute,
|
||||
@@ -82,6 +88,10 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof ResetPasswordImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/signup': {
|
||||
preLoaderRoute: typeof SignupImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/_layout/admin': {
|
||||
preLoaderRoute: typeof LayoutAdminImport
|
||||
parentRoute: typeof LayoutImport
|
||||
@@ -113,6 +123,7 @@ export const routeTree = rootRoute.addChildren([
|
||||
LoginRoute,
|
||||
RecoverPasswordRoute,
|
||||
ResetPasswordRoute,
|
||||
SignupRoute,
|
||||
])
|
||||
|
||||
/* prettier-ignore-end */
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons"
|
||||
import {
|
||||
Button,
|
||||
Center,
|
||||
Container,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
InputGroup,
|
||||
InputRightElement,
|
||||
Link,
|
||||
Text,
|
||||
useBoolean,
|
||||
} from "@chakra-ui/react"
|
||||
import {
|
||||
@@ -126,14 +126,18 @@ function Login() {
|
||||
</InputGroup>
|
||||
{error && <FormErrorMessage>{error}</FormErrorMessage>}
|
||||
</FormControl>
|
||||
<Center>
|
||||
<Link as={RouterLink} to="/recover-password" color="blue.500">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</Center>
|
||||
<Link as={RouterLink} to="/recover-password" color="blue.500">
|
||||
Forgot password?
|
||||
</Link>
|
||||
<Button variant="primary" type="submit" isLoading={isSubmitting}>
|
||||
Log In
|
||||
</Button>
|
||||
<Text>
|
||||
Don't have an account?{" "}
|
||||
<Link as={RouterLink} to="/signup" color="blue.500">
|
||||
Sign up
|
||||
</Link>
|
||||
</Text>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
|
163
frontend/src/routes/signup.tsx
Normal file
163
frontend/src/routes/signup.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormErrorMessage,
|
||||
FormLabel,
|
||||
Image,
|
||||
Input,
|
||||
Link,
|
||||
Text,
|
||||
} from "@chakra-ui/react"
|
||||
import {
|
||||
Link as RouterLink,
|
||||
createFileRoute,
|
||||
redirect,
|
||||
} from "@tanstack/react-router"
|
||||
import { type SubmitHandler, useForm } from "react-hook-form"
|
||||
|
||||
import Logo from "/assets/images/fastapi-logo.svg"
|
||||
import type { UserRegister } from "../client"
|
||||
import useAuth, { isLoggedIn } from "../hooks/useAuth"
|
||||
import { confirmPasswordRules, emailPattern, passwordRules } from "../utils"
|
||||
|
||||
export const Route = createFileRoute("/signup")({
|
||||
component: SignUp,
|
||||
beforeLoad: async () => {
|
||||
if (isLoggedIn()) {
|
||||
throw redirect({
|
||||
to: "/",
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
interface UserRegisterForm extends UserRegister {
|
||||
confirm_password: string
|
||||
}
|
||||
|
||||
function SignUp() {
|
||||
const { signUpMutation } = useAuth()
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
getValues,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<UserRegisterForm>({
|
||||
mode: "onBlur",
|
||||
criteriaMode: "all",
|
||||
defaultValues: {
|
||||
email: "",
|
||||
full_name: "",
|
||||
password: "",
|
||||
confirm_password: "",
|
||||
},
|
||||
})
|
||||
|
||||
const onSubmit: SubmitHandler<UserRegisterForm> = (data) => {
|
||||
signUpMutation.mutate(data)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex flexDir={{ base: "column", md: "row" }} justify="center" h="100vh">
|
||||
<Container
|
||||
as="form"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
h="100vh"
|
||||
maxW="sm"
|
||||
alignItems="stretch"
|
||||
justifyContent="center"
|
||||
gap={4}
|
||||
centerContent
|
||||
>
|
||||
<Image
|
||||
src={Logo}
|
||||
alt="FastAPI logo"
|
||||
height="auto"
|
||||
maxW="2xs"
|
||||
alignSelf="center"
|
||||
mb={4}
|
||||
/>
|
||||
<FormControl id="full_name" isInvalid={!!errors.full_name}>
|
||||
<FormLabel htmlFor="full_name" srOnly>
|
||||
Full Name
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="full_name"
|
||||
minLength={3}
|
||||
{...register("full_name")}
|
||||
placeholder="Full Name"
|
||||
type="text"
|
||||
/>
|
||||
{errors.full_name && (
|
||||
<FormErrorMessage>{errors.full_name.message}</FormErrorMessage>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormControl id="email" isInvalid={!!errors.email}>
|
||||
<FormLabel htmlFor="username" srOnly>
|
||||
Email
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="email"
|
||||
{...register("email", {
|
||||
pattern: emailPattern,
|
||||
})}
|
||||
placeholder="Email"
|
||||
type="email"
|
||||
/>
|
||||
{errors.email && (
|
||||
<FormErrorMessage>{errors.email.message}</FormErrorMessage>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormControl id="password" isInvalid={!!errors.password}>
|
||||
<FormLabel htmlFor="password" srOnly>
|
||||
Password
|
||||
</FormLabel>
|
||||
<Input
|
||||
id="password"
|
||||
{...register("password", passwordRules())}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
/>
|
||||
{errors.password && (
|
||||
<FormErrorMessage>{errors.password.message}</FormErrorMessage>
|
||||
)}
|
||||
</FormControl>
|
||||
<FormControl
|
||||
id="confirm_password"
|
||||
isInvalid={!!errors.confirm_password}
|
||||
>
|
||||
<FormLabel htmlFor="confirm_password" srOnly>
|
||||
Confirm Password
|
||||
</FormLabel>
|
||||
|
||||
<Input
|
||||
id="confirm_password"
|
||||
{...register("confirm_password", confirmPasswordRules(getValues))}
|
||||
placeholder="Repeat Password"
|
||||
type="password"
|
||||
/>
|
||||
{errors.confirm_password && (
|
||||
<FormErrorMessage>
|
||||
{errors.confirm_password.message}
|
||||
</FormErrorMessage>
|
||||
)}
|
||||
</FormControl>
|
||||
<Button variant="primary" type="submit" isLoading={isSubmitting}>
|
||||
Sign Up
|
||||
</Button>
|
||||
<Text>
|
||||
Already have an account?{" "}
|
||||
<Link as={RouterLink} to="/login" color="blue.500">
|
||||
Log In
|
||||
</Link>
|
||||
</Text>
|
||||
</Container>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SignUp
|
Reference in New Issue
Block a user