Skip to content
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