diff --git a/src/App.tsx b/src/App.tsx
index baecd98..8567bc5 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -12,6 +12,7 @@ import AuthContextProvider from "~/AuthContext/AuthContextProvider"
import AuthenticateRoute from "~/routes/AuthenticateRoute"
import AuthenticatedRoute from "~/routes/AuthenticatedRoute"
import CompleteAccountRoute from "~/routes/CompleteAccountRoute"
+import LoginRoute from "~/routes/LoginRoute"
import RootRoute from "~/routes/Root"
import SignupRoute from "~/routes/SignupRoute"
import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
@@ -28,14 +29,19 @@ const router = createBrowserRouter([
children: [
{
loader: getServerSettings,
- path: "/auth/verify-email",
- element: ,
+ path: "/auth/login",
+ element: ,
},
{
loader: getServerSettings,
path: "/auth/signup",
element: ,
},
+ {
+ loader: getServerSettings,
+ path: "/auth/verify-email",
+ element: ,
+ },
{
path: "/auth/complete-account",
element: ,
diff --git a/src/apis/index.ts b/src/apis/index.ts
index 3256d5b..2381b03 100644
--- a/src/apis/index.ts
+++ b/src/apis/index.ts
@@ -18,3 +18,9 @@ export * from "./create-alias"
export {default as createAlias} from "./create-alias"
export * from "./update-account"
export {default as updateAccount} from "./update-account"
+export * from "./login-with-email"
+export {default as loginWithEmail} from "./login-with-email"
+export * from "./verify-login-with-email"
+export {default as verifyLoginWithEmail} from "./verify-login-with-email"
+export * from "./resend-email-login-code"
+export {default as resendEmailLoginCode} from "./resend-email-login-code"
diff --git a/src/apis/login-with-email.ts b/src/apis/login-with-email.ts
new file mode 100644
index 0000000..f3a4e95
--- /dev/null
+++ b/src/apis/login-with-email.ts
@@ -0,0 +1,19 @@
+import {client} from "~/constants/axios-client"
+
+export interface LoginWithEmailResult {
+ detail: string
+ sameRequestToken: string
+}
+
+export default async function loginWithEmail(
+ email: string,
+): Promise {
+ const {data} = await client.post(
+ `${import.meta.env.VITE_SERVER_BASE_URL}/auth/login/email-token`,
+ {
+ email,
+ },
+ )
+
+ return data
+}
diff --git a/src/apis/login.ts b/src/apis/login.ts
deleted file mode 100644
index 63ce312..0000000
--- a/src/apis/login.ts
+++ /dev/null
@@ -1 +0,0 @@
-export default function login() {}
diff --git a/src/apis/logout.ts b/src/apis/logout.ts
index 293532d..a12ad95 100644
--- a/src/apis/logout.ts
+++ b/src/apis/logout.ts
@@ -1,7 +1,7 @@
import {MinimumServerResponse} from "~/server-types"
import {client} from "~/constants/axios-client"
-export default async function logout(): Promise {
+export default async function logout(): Promise {
const {data} = await client.post(
`${import.meta.env.VITE_SERVER_BASE_URL}/auth/logout`,
)
diff --git a/src/apis/resend-email-login-code.ts b/src/apis/resend-email-login-code.ts
new file mode 100644
index 0000000..374345e
--- /dev/null
+++ b/src/apis/resend-email-login-code.ts
@@ -0,0 +1,24 @@
+import {SimpleDetailResponse} from "~/server-types"
+import {client} from "~/constants/axios-client"
+
+export interface ResendEmailLoginCodeData {
+ email: string
+ sameRequestToken: string
+}
+
+export default async function resendEmailLoginCode({
+ email,
+ sameRequestToken,
+}: ResendEmailLoginCodeData): Promise {
+ const {data} = await client.post(
+ `${
+ import.meta.env.VITE_SERVER_BASE_URL
+ }/auth/login/email-token/resend-email`,
+ {
+ email,
+ sameRequestToken,
+ },
+ )
+
+ return data
+}
diff --git a/src/apis/resend-email-verification-code.ts b/src/apis/resend-email-verification-code.ts
index 78f03c3..4682c5f 100644
--- a/src/apis/resend-email-verification-code.ts
+++ b/src/apis/resend-email-verification-code.ts
@@ -1,9 +1,9 @@
-import {MinimumServerResponse} from "~/server-types"
+import {SimpleDetailResponse} from "~/server-types"
import {client} from "~/constants/axios-client"
export default async function resendEmailVerificationCode(
email: string,
-): Promise {
+): Promise {
const {data} = await client.post(
`${import.meta.env.VITE_SERVER_BASE_URL}/auth/resend-email`,
{
diff --git a/src/apis/verify-login-with-email.ts b/src/apis/verify-login-with-email.ts
new file mode 100644
index 0000000..c74b68a
--- /dev/null
+++ b/src/apis/verify-login-with-email.ts
@@ -0,0 +1,32 @@
+import {AuthenticationDetails} from "~/server-types"
+import {client} from "~/constants/axios-client"
+import parseUser from "~/apis/helpers/parse-user"
+
+export interface VerifyLoginWithEmailData {
+ email: string
+ token: string
+ sameRequestToken: string
+}
+
+export default async function verifyLoginWithEmail({
+ email,
+ token,
+ sameRequestToken,
+}: VerifyLoginWithEmailData): Promise {
+ const {data} = await client.post(
+ `${import.meta.env.VITE_SERVER_BASE_URL}/auth/login/email-token/verify`,
+ {
+ email,
+ token,
+ sameRequestToken,
+ },
+ {
+ withCredentials: true,
+ },
+ )
+
+ return {
+ ...data,
+ user: parseUser(data.user),
+ }
+}
diff --git a/src/components/MutationStatusSnackbar.tsx b/src/components/MutationStatusSnackbar.tsx
index 6d3d28b..85e85a8 100644
--- a/src/components/MutationStatusSnackbar.tsx
+++ b/src/components/MutationStatusSnackbar.tsx
@@ -5,7 +5,7 @@ import {UseMutationResult} from "@tanstack/react-query"
import {Alert, AlertProps, Snackbar} from "@mui/material"
import {FastAPIError} from "~/utils"
-import {MinimumServerResponse} from "~/server-types"
+import {SimpleDetailResponse} from "~/server-types"
import getErrorMessage from "~/utils/get-error-message"
export interface MutationStatusSnackbarProps<
@@ -21,7 +21,7 @@ export interface MutationStatusSnackbarProps<
}
export default function MutationStatusSnackbar<
- TData extends MinimumServerResponse = MinimumServerResponse,
+ TData extends SimpleDetailResponse = SimpleDetailResponse,
TError extends AxiosError = AxiosError,
TVariables = unknown,
TContext = unknown,
diff --git a/src/components/TimedButton.tsx b/src/components/TimedButton.tsx
new file mode 100644
index 0000000..dcc56f2
--- /dev/null
+++ b/src/components/TimedButton.tsx
@@ -0,0 +1,37 @@
+import {ReactElement} from "react"
+import differenceInSeconds from "date-fns/differenceInSeconds"
+
+import {LoadingButton, LoadingButtonProps} from "@mui/lab"
+
+import {useIntervalUpdate} from "~/hooks"
+import {isDev} from "~/constants/development"
+
+export interface TimedButtonProps extends LoadingButtonProps {
+ interval: number
+}
+
+export default function TimedButton({
+ interval,
+ children,
+ onClick,
+ disabled: parentDisabled = false,
+ ...props
+}: TimedButtonProps): ReactElement {
+ const [startDate, resetInterval] = useIntervalUpdate(1000)
+ const secondsPassed = differenceInSeconds(new Date(), startDate)
+ const secondsLeft = (isDev ? 3 : interval) - secondsPassed
+
+ return (
+ 0}
+ onClick={event => {
+ resetInterval()
+ onClick?.(event)
+ }}
+ >
+ {children}
+ {secondsLeft > 0 && ({secondsLeft})}
+
+ )
+}
diff --git a/src/components/index.ts b/src/components/index.ts
index 702f9f5..a27bd0d 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -10,3 +10,5 @@ export * from "./SimpleForm"
export {default as SimpleForm} from "./SimpleForm"
export * from "./MutationStatusSnackbar"
export {default as MutationStatusSnackbar} from "./MutationStatusSnackbar"
+export * from "./TimedButton"
+export {default as TimedButton} from "./TimedButton"
diff --git a/src/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm.tsx b/src/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm.tsx
new file mode 100644
index 0000000..fde920f
--- /dev/null
+++ b/src/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm.tsx
@@ -0,0 +1,147 @@
+import {AxiosError} from "axios"
+import {ReactElement} from "react"
+import {useFormik} from "formik"
+import {FaHashtag} from "react-icons/fa"
+import {MdChevronRight, MdMail} from "react-icons/md"
+
+import {useMutation} from "@tanstack/react-query"
+import {Box, Grid, InputAdornment, TextField, Typography} from "@mui/material"
+import {LoadingButton} from "@mui/lab"
+
+import {AuthenticationDetails, ServerUser} from "~/server-types"
+import {VerifyLoginWithEmailData, verifyLoginWithEmail} from "~/apis"
+import {MultiStepFormElement} from "~/components"
+import {parseFastapiError} from "~/utils"
+
+import ResendMailButton from "./ResendMailButton"
+import useSchema from "./use-schema"
+
+export interface ConfirmCodeFormProps {
+ onConfirm: (user: ServerUser) => void
+ email: string
+ sameRequestToken: string
+}
+
+interface Form {
+ code: string
+ detail: string
+}
+
+export default function ConfirmCodeForm({
+ onConfirm,
+ email,
+ sameRequestToken,
+}: ConfirmCodeFormProps): ReactElement {
+ const schema = useSchema()
+ const {mutateAsync} = useMutation<
+ AuthenticationDetails,
+ AxiosError,
+ VerifyLoginWithEmailData
+ >(verifyLoginWithEmail, {
+ onSuccess: ({user}) => onConfirm(user),
+ })
+ const formik = useFormik