mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 15:55:26 +02:00
added login page; improved resend mails
This commit is contained in:
parent
bb4c58508d
commit
2083c0bdb6
10
src/App.tsx
10
src/App.tsx
@ -12,6 +12,7 @@ import AuthContextProvider from "~/AuthContext/AuthContextProvider"
|
|||||||
import AuthenticateRoute from "~/routes/AuthenticateRoute"
|
import AuthenticateRoute from "~/routes/AuthenticateRoute"
|
||||||
import AuthenticatedRoute from "~/routes/AuthenticatedRoute"
|
import AuthenticatedRoute from "~/routes/AuthenticatedRoute"
|
||||||
import CompleteAccountRoute from "~/routes/CompleteAccountRoute"
|
import CompleteAccountRoute from "~/routes/CompleteAccountRoute"
|
||||||
|
import LoginRoute from "~/routes/LoginRoute"
|
||||||
import RootRoute from "~/routes/Root"
|
import RootRoute from "~/routes/Root"
|
||||||
import SignupRoute from "~/routes/SignupRoute"
|
import SignupRoute from "~/routes/SignupRoute"
|
||||||
import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
|
import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
|
||||||
@ -28,14 +29,19 @@ const router = createBrowserRouter([
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
loader: getServerSettings,
|
loader: getServerSettings,
|
||||||
path: "/auth/verify-email",
|
path: "/auth/login",
|
||||||
element: <VerifyEmailRoute />,
|
element: <LoginRoute />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loader: getServerSettings,
|
loader: getServerSettings,
|
||||||
path: "/auth/signup",
|
path: "/auth/signup",
|
||||||
element: <SignupRoute />,
|
element: <SignupRoute />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
loader: getServerSettings,
|
||||||
|
path: "/auth/verify-email",
|
||||||
|
element: <VerifyEmailRoute />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/auth/complete-account",
|
path: "/auth/complete-account",
|
||||||
element: <CompleteAccountRoute />,
|
element: <CompleteAccountRoute />,
|
||||||
|
@ -18,3 +18,9 @@ export * from "./create-alias"
|
|||||||
export {default as createAlias} from "./create-alias"
|
export {default as createAlias} from "./create-alias"
|
||||||
export * from "./update-account"
|
export * from "./update-account"
|
||||||
export {default as updateAccount} 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"
|
||||||
|
19
src/apis/login-with-email.ts
Normal file
19
src/apis/login-with-email.ts
Normal file
@ -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<LoginWithEmailResult> {
|
||||||
|
const {data} = await client.post(
|
||||||
|
`${import.meta.env.VITE_SERVER_BASE_URL}/auth/login/email-token`,
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
@ -1 +0,0 @@
|
|||||||
export default function login() {}
|
|
@ -1,7 +1,7 @@
|
|||||||
import {MinimumServerResponse} from "~/server-types"
|
import {MinimumServerResponse} from "~/server-types"
|
||||||
import {client} from "~/constants/axios-client"
|
import {client} from "~/constants/axios-client"
|
||||||
|
|
||||||
export default async function logout(): Promise<MinimumServerResponse> {
|
export default async function logout(): Promise<SimpleDetailResponse> {
|
||||||
const {data} = await client.post(
|
const {data} = await client.post(
|
||||||
`${import.meta.env.VITE_SERVER_BASE_URL}/auth/logout`,
|
`${import.meta.env.VITE_SERVER_BASE_URL}/auth/logout`,
|
||||||
)
|
)
|
||||||
|
24
src/apis/resend-email-login-code.ts
Normal file
24
src/apis/resend-email-login-code.ts
Normal file
@ -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<SimpleDetailResponse> {
|
||||||
|
const {data} = await client.post(
|
||||||
|
`${
|
||||||
|
import.meta.env.VITE_SERVER_BASE_URL
|
||||||
|
}/auth/login/email-token/resend-email`,
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
sameRequestToken,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
import {MinimumServerResponse} from "~/server-types"
|
import {SimpleDetailResponse} from "~/server-types"
|
||||||
import {client} from "~/constants/axios-client"
|
import {client} from "~/constants/axios-client"
|
||||||
|
|
||||||
export default async function resendEmailVerificationCode(
|
export default async function resendEmailVerificationCode(
|
||||||
email: string,
|
email: string,
|
||||||
): Promise<MinimumServerResponse> {
|
): Promise<SimpleDetailResponse> {
|
||||||
const {data} = await client.post(
|
const {data} = await client.post(
|
||||||
`${import.meta.env.VITE_SERVER_BASE_URL}/auth/resend-email`,
|
`${import.meta.env.VITE_SERVER_BASE_URL}/auth/resend-email`,
|
||||||
{
|
{
|
||||||
|
32
src/apis/verify-login-with-email.ts
Normal file
32
src/apis/verify-login-with-email.ts
Normal file
@ -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<AuthenticationDetails> {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@ import {UseMutationResult} from "@tanstack/react-query"
|
|||||||
import {Alert, AlertProps, Snackbar} from "@mui/material"
|
import {Alert, AlertProps, Snackbar} from "@mui/material"
|
||||||
|
|
||||||
import {FastAPIError} from "~/utils"
|
import {FastAPIError} from "~/utils"
|
||||||
import {MinimumServerResponse} from "~/server-types"
|
import {SimpleDetailResponse} from "~/server-types"
|
||||||
import getErrorMessage from "~/utils/get-error-message"
|
import getErrorMessage from "~/utils/get-error-message"
|
||||||
|
|
||||||
export interface MutationStatusSnackbarProps<
|
export interface MutationStatusSnackbarProps<
|
||||||
@ -21,7 +21,7 @@ export interface MutationStatusSnackbarProps<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MutationStatusSnackbar<
|
export default function MutationStatusSnackbar<
|
||||||
TData extends MinimumServerResponse = MinimumServerResponse,
|
TData extends SimpleDetailResponse = SimpleDetailResponse,
|
||||||
TError extends AxiosError = AxiosError<FastAPIError>,
|
TError extends AxiosError = AxiosError<FastAPIError>,
|
||||||
TVariables = unknown,
|
TVariables = unknown,
|
||||||
TContext = unknown,
|
TContext = unknown,
|
||||||
|
37
src/components/TimedButton.tsx
Normal file
37
src/components/TimedButton.tsx
Normal file
@ -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 (
|
||||||
|
<LoadingButton
|
||||||
|
{...props}
|
||||||
|
disabled={parentDisabled || secondsLeft > 0}
|
||||||
|
onClick={event => {
|
||||||
|
resetInterval()
|
||||||
|
onClick?.(event)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>{children}</span>
|
||||||
|
{secondsLeft > 0 && <span> ({secondsLeft})</span>}
|
||||||
|
</LoadingButton>
|
||||||
|
)
|
||||||
|
}
|
@ -10,3 +10,5 @@ export * from "./SimpleForm"
|
|||||||
export {default as SimpleForm} from "./SimpleForm"
|
export {default as SimpleForm} from "./SimpleForm"
|
||||||
export * from "./MutationStatusSnackbar"
|
export * from "./MutationStatusSnackbar"
|
||||||
export {default as MutationStatusSnackbar} from "./MutationStatusSnackbar"
|
export {default as MutationStatusSnackbar} from "./MutationStatusSnackbar"
|
||||||
|
export * from "./TimedButton"
|
||||||
|
export {default as TimedButton} from "./TimedButton"
|
||||||
|
147
src/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm.tsx
Normal file
147
src/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm.tsx
Normal file
@ -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<Form>({
|
||||||
|
validationSchema: schema,
|
||||||
|
initialValues: {
|
||||||
|
code: "",
|
||||||
|
detail: "",
|
||||||
|
},
|
||||||
|
onSubmit: async (values, {setErrors}) => {
|
||||||
|
try {
|
||||||
|
await mutateAsync({
|
||||||
|
email,
|
||||||
|
sameRequestToken,
|
||||||
|
token: values.code,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
setErrors(parseFastapiError(error as AxiosError))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiStepFormElement>
|
||||||
|
<form onSubmit={formik.handleSubmit}>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={4}
|
||||||
|
padding={4}
|
||||||
|
justifyContent="center"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="h6" component="h1" align="center">
|
||||||
|
You got mail!
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Box display="flex" justifyContent="center">
|
||||||
|
<MdMail size={64} />
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
component="p"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
We sent you a code to your email. Enter it below to
|
||||||
|
login.
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<TextField
|
||||||
|
key="code"
|
||||||
|
fullWidth
|
||||||
|
name="code"
|
||||||
|
id="code"
|
||||||
|
label="code"
|
||||||
|
value={formik.values.code}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
error={
|
||||||
|
formik.touched.code &&
|
||||||
|
Boolean(formik.errors.code)
|
||||||
|
}
|
||||||
|
helperText={
|
||||||
|
formik.touched.code && formik.errors.code
|
||||||
|
}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<FaHashtag />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Grid
|
||||||
|
width="100%"
|
||||||
|
container
|
||||||
|
display="flex"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Grid item>
|
||||||
|
<ResendMailButton
|
||||||
|
email={email}
|
||||||
|
sameRequestToken={sameRequestToken}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<LoadingButton
|
||||||
|
loading={formik.isSubmitting}
|
||||||
|
variant="contained"
|
||||||
|
type="submit"
|
||||||
|
startIcon={<MdChevronRight />}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</LoadingButton>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</form>
|
||||||
|
</MultiStepFormElement>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
import {AxiosError} from "axios"
|
||||||
|
import {useLoaderData} from "react-router-dom"
|
||||||
|
import React, {ReactElement} from "react"
|
||||||
|
|
||||||
|
import {useMutation} from "@tanstack/react-query"
|
||||||
|
|
||||||
|
import {resendEmailLoginCode} from "~/apis"
|
||||||
|
import {MutationStatusSnackbar, TimedButton} from "~/components"
|
||||||
|
import {ServerSettings, SimpleDetailResponse} from "~/server-types"
|
||||||
|
import {MdMail} from "react-icons/md"
|
||||||
|
|
||||||
|
export interface ResendMailButtonProps {
|
||||||
|
email: string
|
||||||
|
sameRequestToken: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ResendMailButton({
|
||||||
|
email,
|
||||||
|
sameRequestToken,
|
||||||
|
}: ResendMailButtonProps): ReactElement {
|
||||||
|
const settings = useLoaderData() as ServerSettings
|
||||||
|
|
||||||
|
const mutation = useMutation<SimpleDetailResponse, AxiosError, void>(() =>
|
||||||
|
resendEmailLoginCode({
|
||||||
|
email,
|
||||||
|
sameRequestToken,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const {mutate} = mutation
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TimedButton
|
||||||
|
interval={settings.emailResendWaitTime}
|
||||||
|
startIcon={<MdMail />}
|
||||||
|
onClick={() => mutate()}
|
||||||
|
>
|
||||||
|
Resend Mail
|
||||||
|
</TimedButton>
|
||||||
|
<MutationStatusSnackbar mutation={mutation} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
25
src/route-widgets/LoginRoute/ConfirmCodeForm/use-schema.ts
Normal file
25
src/route-widgets/LoginRoute/ConfirmCodeForm/use-schema.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import * as yup from "yup"
|
||||||
|
import {useLoaderData} from "react-router-dom"
|
||||||
|
|
||||||
|
import {ServerSettings} from "~/server-types"
|
||||||
|
|
||||||
|
export default function useSchema(): yup.ObjectSchema<any> {
|
||||||
|
const settings = useLoaderData() as ServerSettings
|
||||||
|
|
||||||
|
return yup.object().shape({
|
||||||
|
code: yup
|
||||||
|
.string()
|
||||||
|
.required()
|
||||||
|
.min(settings.emailLoginTokenLength)
|
||||||
|
.max(settings.emailLoginTokenLength)
|
||||||
|
.test("chars", "This code is not valid.", code => {
|
||||||
|
if (!code) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const chars = settings.emailLoginTokenChars.split("")
|
||||||
|
|
||||||
|
return code.split("").every(char => chars.includes(char))
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
91
src/route-widgets/LoginRoute/EmailForm.tsx
Normal file
91
src/route-widgets/LoginRoute/EmailForm.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import * as yup from "yup"
|
||||||
|
import {ReactElement} from "react"
|
||||||
|
import {AxiosError} from "axios"
|
||||||
|
import {useFormik} from "formik"
|
||||||
|
import {MdEmail} from "react-icons/md"
|
||||||
|
|
||||||
|
import {useMutation} from "@tanstack/react-query"
|
||||||
|
import {InputAdornment, TextField} from "@mui/material"
|
||||||
|
|
||||||
|
import {LoginWithEmailResult, loginWithEmail} from "~/apis"
|
||||||
|
import {parseFastapiError} from "~/utils"
|
||||||
|
import {MultiStepFormElement, SimpleForm} from "~/components"
|
||||||
|
|
||||||
|
export interface EmailFormProps {
|
||||||
|
onLogin: (email: string, sameRequestToken: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Form {
|
||||||
|
email: string
|
||||||
|
detail: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCHEMA = yup.object().shape({
|
||||||
|
email: yup.string().email().required(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function EmailForm({onLogin}: EmailFormProps): ReactElement {
|
||||||
|
const {mutateAsync} = useMutation<LoginWithEmailResult, AxiosError, string>(
|
||||||
|
loginWithEmail,
|
||||||
|
{
|
||||||
|
onSuccess: ({sameRequestToken}) =>
|
||||||
|
onLogin(formik.values.email, sameRequestToken),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const formik = useFormik<Form>({
|
||||||
|
validationSchema: SCHEMA,
|
||||||
|
initialValues: {
|
||||||
|
email: "",
|
||||||
|
detail: "",
|
||||||
|
},
|
||||||
|
onSubmit: async (values, {setErrors}) => {
|
||||||
|
try {
|
||||||
|
await mutateAsync(values.email)
|
||||||
|
} catch (error) {
|
||||||
|
setErrors(parseFastapiError(error as AxiosError))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiStepFormElement>
|
||||||
|
<form onSubmit={formik.handleSubmit}>
|
||||||
|
<SimpleForm
|
||||||
|
title="Sign in"
|
||||||
|
description="We'll send you a verification code to your email."
|
||||||
|
continueActionLabel="Send code"
|
||||||
|
nonFieldError={formik.errors.detail}
|
||||||
|
isSubmitting={formik.isSubmitting}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
<TextField
|
||||||
|
key="email"
|
||||||
|
fullWidth
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
label="Email"
|
||||||
|
inputMode="email"
|
||||||
|
value={formik.values.email}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
error={
|
||||||
|
formik.touched.email &&
|
||||||
|
Boolean(formik.errors.email)
|
||||||
|
}
|
||||||
|
helperText={
|
||||||
|
formik.touched.email && formik.errors.email
|
||||||
|
}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<MdEmail />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
</SimpleForm>
|
||||||
|
</form>
|
||||||
|
</MultiStepFormElement>
|
||||||
|
)
|
||||||
|
}
|
@ -1,52 +1,37 @@
|
|||||||
import {MdEmail} from "react-icons/md"
|
|
||||||
import {AxiosError} from "axios"
|
import {AxiosError} from "axios"
|
||||||
|
import {useLoaderData} from "react-router-dom"
|
||||||
|
import {MdMail} from "react-icons/md"
|
||||||
import React, {ReactElement} from "react"
|
import React, {ReactElement} from "react"
|
||||||
import differenceInSeconds from "date-fns/differenceInSeconds"
|
|
||||||
|
|
||||||
import {useMutation} from "@tanstack/react-query"
|
import {useMutation} from "@tanstack/react-query"
|
||||||
import {LoadingButton} from "@mui/lab"
|
|
||||||
|
|
||||||
import {useIntervalUpdate} from "~/hooks"
|
|
||||||
import {resendEmailVerificationCode} from "~/apis"
|
import {resendEmailVerificationCode} from "~/apis"
|
||||||
import {isDev} from "~/constants/development"
|
import {MutationStatusSnackbar, TimedButton} from "~/components"
|
||||||
import {MutationStatusSnackbar} from "~/components"
|
import {ServerSettings, SimpleDetailResponse} from "~/server-types"
|
||||||
import {MinimumServerResponse} from "~/server-types"
|
|
||||||
|
|
||||||
export interface ResendMailButtonProps {
|
export interface ResendMailButtonProps {
|
||||||
email: string
|
email: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const RESEND_INTERVAL = isDev ? 3 : 60
|
|
||||||
|
|
||||||
export default function ResendMailButton({
|
export default function ResendMailButton({
|
||||||
email,
|
email,
|
||||||
}: ResendMailButtonProps): ReactElement {
|
}: ResendMailButtonProps): ReactElement {
|
||||||
const [startDate, resetInterval] = useIntervalUpdate(1000)
|
const settings = useLoaderData() as ServerSettings
|
||||||
const secondsPassed = differenceInSeconds(new Date(), startDate)
|
|
||||||
const secondsLeft = RESEND_INTERVAL - secondsPassed
|
|
||||||
|
|
||||||
const mutation = useMutation<MinimumServerResponse, AxiosError, string>(
|
const mutation = useMutation<SimpleDetailResponse, AxiosError, void>(() =>
|
||||||
resendEmailVerificationCode,
|
resendEmailVerificationCode(email),
|
||||||
{
|
|
||||||
onSettled: resetInterval,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
const {mutate: resendEmail, isLoading} = mutation
|
const {mutate} = mutation
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LoadingButton
|
<TimedButton
|
||||||
variant="contained"
|
interval={settings.emailResendWaitTime}
|
||||||
startIcon={<MdEmail />}
|
startIcon={<MdMail />}
|
||||||
onClick={() => resendEmail(email)}
|
onClick={() => mutate()}
|
||||||
loading={isLoading}
|
|
||||||
disabled={secondsLeft > 0 || isLoading}
|
|
||||||
>
|
>
|
||||||
<span>
|
Resend Mail
|
||||||
<span>Resend Email</span>
|
</TimedButton>
|
||||||
{secondsLeft > 0 && <span> ({secondsLeft})</span>}
|
|
||||||
</span>
|
|
||||||
</LoadingButton>
|
|
||||||
<MutationStatusSnackbar mutation={mutation} />
|
<MutationStatusSnackbar mutation={mutation} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
36
src/routes/LoginRoute.tsx
Normal file
36
src/routes/LoginRoute.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import {ReactElement, useContext, useState} from "react"
|
||||||
|
import {useNavigate} from "react-router-dom"
|
||||||
|
|
||||||
|
import {MultiStepForm} from "~/components"
|
||||||
|
import AuthContext from "~/AuthContext/AuthContext"
|
||||||
|
import ConfirmCodeForm from "~/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm"
|
||||||
|
import EmailForm from "~/route-widgets/LoginRoute/EmailForm"
|
||||||
|
|
||||||
|
export default function LoginRoute(): ReactElement {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const {login} = useContext(AuthContext)
|
||||||
|
|
||||||
|
const [email, setEmail] = useState<string>("")
|
||||||
|
const [sameRequestToken, setSameRequestToken] = useState<string>("")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiStepForm
|
||||||
|
steps={[
|
||||||
|
<EmailForm
|
||||||
|
key="email_form"
|
||||||
|
onLogin={(email, sameRequestToken) => {
|
||||||
|
setEmail(email)
|
||||||
|
setSameRequestToken(sameRequestToken)
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
<ConfirmCodeForm
|
||||||
|
key="confirm_code_form"
|
||||||
|
onConfirm={user => login(user, () => navigate("/"))}
|
||||||
|
email={email}
|
||||||
|
sameRequestToken={sameRequestToken}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
index={email === "" ? 0 : 1}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@ -52,11 +52,14 @@ export interface ServerUser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthenticationDetails {
|
export interface SimpleDetailResponse {
|
||||||
user: ServerUser
|
|
||||||
detail: string
|
detail: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AuthenticationDetails extends SimpleDetailResponse {
|
||||||
|
user: ServerUser
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerSettings {
|
export interface ServerSettings {
|
||||||
mailDomain: string
|
mailDomain: string
|
||||||
randomEmailIdMinLength: number
|
randomEmailIdMinLength: number
|
||||||
@ -68,10 +71,9 @@ export interface ServerSettings {
|
|||||||
otherRelayDomains: Array<string>
|
otherRelayDomains: Array<string>
|
||||||
emailVerificationChars: string
|
emailVerificationChars: string
|
||||||
emailVerificationLength: number
|
emailVerificationLength: number
|
||||||
}
|
emailLoginTokenChars: string
|
||||||
|
emailLoginTokenLength: number
|
||||||
export interface MinimumServerResponse {
|
emailResendWaitTime: number
|
||||||
detail?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Alias {
|
export interface Alias {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user