Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add Sign Up and make OPEN_USER_REGISTRATION=True by default #1265

Merged
merged 10 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,h
SECRET_KEY=changethis
FIRST_SUPERUSER=admin@example.com
FIRST_SUPERUSER_PASSWORD=changethis
USERS_OPEN_REGISTRATION=False
USERS_OPEN_REGISTRATION=True

# Emails
SMTP_HOST=
Expand Down
29 changes: 28 additions & 1 deletion frontend/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -57,6 +83,7 @@ const useAuth = () => {
}

return {
signUpMutation,
loginMutation,
logout,
user,
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -113,6 +123,7 @@ export const routeTree = rootRoute.addChildren([
LoginRoute,
RecoverPasswordRoute,
ResetPasswordRoute,
SignupRoute,
])

/* prettier-ignore-end */
16 changes: 10 additions & 6 deletions frontend/src/routes/login.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { ViewIcon, ViewOffIcon } from "@chakra-ui/icons"
import {
Button,
Center,
Container,
FormControl,
FormErrorMessage,
Expand All @@ -11,6 +10,7 @@ import {
InputGroup,
InputRightElement,
Link,
Text,
useBoolean,
} from "@chakra-ui/react"
import {
Expand Down Expand Up @@ -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>
</>
)
Expand Down
163 changes: 163 additions & 0 deletions frontend/src/routes/signup.tsx
Original file line number Diff line number Diff line change
@@ -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