✨ Add Sign Up and make OPEN_USER_REGISTRATION=True
by default (#1265)
This commit is contained in:
2
.env
2
.env
@@ -13,7 +13,7 @@ BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,h
|
|||||||
SECRET_KEY=changethis
|
SECRET_KEY=changethis
|
||||||
FIRST_SUPERUSER=admin@example.com
|
FIRST_SUPERUSER=admin@example.com
|
||||||
FIRST_SUPERUSER_PASSWORD=changethis
|
FIRST_SUPERUSER_PASSWORD=changethis
|
||||||
USERS_OPEN_REGISTRATION=False
|
USERS_OPEN_REGISTRATION=True
|
||||||
|
|
||||||
# Emails
|
# Emails
|
||||||
SMTP_HOST=
|
SMTP_HOST=
|
||||||
|
@@ -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 { useNavigate } from "@tanstack/react-router"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
@@ -8,8 +8,10 @@ import {
|
|||||||
type ApiError,
|
type ApiError,
|
||||||
LoginService,
|
LoginService,
|
||||||
type UserPublic,
|
type UserPublic,
|
||||||
|
type UserRegister,
|
||||||
UsersService,
|
UsersService,
|
||||||
} from "../client"
|
} from "../client"
|
||||||
|
import useCustomToast from "./useCustomToast"
|
||||||
|
|
||||||
const isLoggedIn = () => {
|
const isLoggedIn = () => {
|
||||||
return localStorage.getItem("access_token") !== null
|
return localStorage.getItem("access_token") !== null
|
||||||
@@ -18,12 +20,36 @@ const isLoggedIn = () => {
|
|||||||
const useAuth = () => {
|
const useAuth = () => {
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const showToast = useCustomToast()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
const { data: user, isLoading } = useQuery<UserPublic | null, Error>({
|
const { data: user, isLoading } = useQuery<UserPublic | null, Error>({
|
||||||
queryKey: ["currentUser"],
|
queryKey: ["currentUser"],
|
||||||
queryFn: UsersService.readUserMe,
|
queryFn: UsersService.readUserMe,
|
||||||
enabled: isLoggedIn(),
|
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 login = async (data: AccessToken) => {
|
||||||
const response = await LoginService.loginAccessToken({
|
const response = await LoginService.loginAccessToken({
|
||||||
formData: data,
|
formData: data,
|
||||||
@@ -57,6 +83,7 @@ const useAuth = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
signUpMutation,
|
||||||
loginMutation,
|
loginMutation,
|
||||||
logout,
|
logout,
|
||||||
user,
|
user,
|
||||||
|
@@ -11,6 +11,7 @@
|
|||||||
// Import Routes
|
// Import Routes
|
||||||
|
|
||||||
import { Route as rootRoute } from './routes/__root'
|
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 ResetPasswordImport } from './routes/reset-password'
|
||||||
import { Route as RecoverPasswordImport } from './routes/recover-password'
|
import { Route as RecoverPasswordImport } from './routes/recover-password'
|
||||||
import { Route as LoginImport } from './routes/login'
|
import { Route as LoginImport } from './routes/login'
|
||||||
@@ -22,6 +23,11 @@ import { Route as LayoutAdminImport } from './routes/_layout/admin'
|
|||||||
|
|
||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
|
const SignupRoute = SignupImport.update({
|
||||||
|
path: '/signup',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const ResetPasswordRoute = ResetPasswordImport.update({
|
const ResetPasswordRoute = ResetPasswordImport.update({
|
||||||
path: '/reset-password',
|
path: '/reset-password',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
@@ -82,6 +88,10 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof ResetPasswordImport
|
preLoaderRoute: typeof ResetPasswordImport
|
||||||
parentRoute: typeof rootRoute
|
parentRoute: typeof rootRoute
|
||||||
}
|
}
|
||||||
|
'/signup': {
|
||||||
|
preLoaderRoute: typeof SignupImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/_layout/admin': {
|
'/_layout/admin': {
|
||||||
preLoaderRoute: typeof LayoutAdminImport
|
preLoaderRoute: typeof LayoutAdminImport
|
||||||
parentRoute: typeof LayoutImport
|
parentRoute: typeof LayoutImport
|
||||||
@@ -113,6 +123,7 @@ export const routeTree = rootRoute.addChildren([
|
|||||||
LoginRoute,
|
LoginRoute,
|
||||||
RecoverPasswordRoute,
|
RecoverPasswordRoute,
|
||||||
ResetPasswordRoute,
|
ResetPasswordRoute,
|
||||||
|
SignupRoute,
|
||||||
])
|
])
|
||||||
|
|
||||||
/* prettier-ignore-end */
|
/* prettier-ignore-end */
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons"
|
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons"
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Center,
|
|
||||||
Container,
|
Container,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormErrorMessage,
|
FormErrorMessage,
|
||||||
@@ -11,6 +10,7 @@ import {
|
|||||||
InputGroup,
|
InputGroup,
|
||||||
InputRightElement,
|
InputRightElement,
|
||||||
Link,
|
Link,
|
||||||
|
Text,
|
||||||
useBoolean,
|
useBoolean,
|
||||||
} from "@chakra-ui/react"
|
} from "@chakra-ui/react"
|
||||||
import {
|
import {
|
||||||
@@ -126,14 +126,18 @@ function Login() {
|
|||||||
</InputGroup>
|
</InputGroup>
|
||||||
{error && <FormErrorMessage>{error}</FormErrorMessage>}
|
{error && <FormErrorMessage>{error}</FormErrorMessage>}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Center>
|
<Link as={RouterLink} to="/recover-password" color="blue.500">
|
||||||
<Link as={RouterLink} to="/recover-password" color="blue.500">
|
Forgot password?
|
||||||
Forgot password?
|
</Link>
|
||||||
</Link>
|
|
||||||
</Center>
|
|
||||||
<Button variant="primary" type="submit" isLoading={isSubmitting}>
|
<Button variant="primary" type="submit" isLoading={isSubmitting}>
|
||||||
Log In
|
Log In
|
||||||
</Button>
|
</Button>
|
||||||
|
<Text>
|
||||||
|
Don't have an account?{" "}
|
||||||
|
<Link as={RouterLink} to="/signup" color="blue.500">
|
||||||
|
Sign up
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
</Container>
|
</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