mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 15:55:26 +02:00
improvements; developed verify email page
This commit is contained in:
parent
f35a07efc1
commit
c8c774de54
26
src/App.tsx
26
src/App.tsx
@ -6,13 +6,35 @@ import {CssBaseline, ThemeProvider} from "@mui/material"
|
|||||||
|
|
||||||
import {queryClient} from "~/constants/react-query"
|
import {queryClient} from "~/constants/react-query"
|
||||||
import {lightTheme} from "~/constants/themes"
|
import {lightTheme} from "~/constants/themes"
|
||||||
import LoadCriticalContent from "~/LoadCriticalContent"
|
import {getServerSettings} from "~/apis"
|
||||||
import RootRoute from "~/routes/Root"
|
import RootRoute from "~/routes/Root"
|
||||||
|
import SignupRoute from "~/routes/SignupRoute"
|
||||||
|
import SingleElementRoute from "~/routes/SingleElementRoute"
|
||||||
|
import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <RootRoute />,
|
element: <RootRoute />,
|
||||||
|
errorElement: <div></div>,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
element: <SingleElementRoute />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
loader: getServerSettings,
|
||||||
|
path: "/verify-email",
|
||||||
|
element: <VerifyEmailRoute />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: getServerSettings,
|
||||||
|
path: "/signup",
|
||||||
|
element: <SignupRoute />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -22,9 +44,7 @@ export default function App(): ReactElement {
|
|||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider theme={lightTheme}>
|
<ThemeProvider theme={lightTheme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<LoadCriticalContent>
|
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</LoadCriticalContent>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
|
@ -1,44 +0,0 @@
|
|||||||
import * as inSeconds from "in-seconds"
|
|
||||||
import {AxiosError} from "axios"
|
|
||||||
import React, {ReactElement} from "react"
|
|
||||||
|
|
||||||
import {useQuery} from "@tanstack/react-query"
|
|
||||||
|
|
||||||
import {getServerSettings} from "~/apis"
|
|
||||||
import LoadingScreen from "~/LoadingScreen"
|
|
||||||
|
|
||||||
import ServerSettingsContext, {
|
|
||||||
ServerSettingsContextType,
|
|
||||||
} from "./ServerSettingsContext"
|
|
||||||
|
|
||||||
export interface LoadCriticalContentProps {
|
|
||||||
children: ReactElement
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LoadCriticalContent({
|
|
||||||
children,
|
|
||||||
}: LoadCriticalContentProps): ReactElement {
|
|
||||||
const {data} = useQuery<ServerSettingsContextType, AxiosError>(
|
|
||||||
["server-settings"],
|
|
||||||
async () => {
|
|
||||||
const settings = await getServerSettings()
|
|
||||||
|
|
||||||
return {
|
|
||||||
serverSettings: settings,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
staleTime: inSeconds.days(20),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return <LoadingScreen />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ServerSettingsContext.Provider value={data}>
|
|
||||||
{children}
|
|
||||||
</ServerSettingsContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
@ -2,14 +2,10 @@ import React, {ReactElement} from "react"
|
|||||||
|
|
||||||
import {Typography} from "@mui/material"
|
import {Typography} from "@mui/material"
|
||||||
|
|
||||||
import {SingleElementWrapper} from "~/components"
|
|
||||||
|
|
||||||
export default function LoadingScreen(): ReactElement {
|
export default function LoadingScreen(): ReactElement {
|
||||||
return (
|
return (
|
||||||
<SingleElementWrapper>
|
|
||||||
<Typography variant="caption" component="p">
|
<Typography variant="caption" component="p">
|
||||||
Loading...
|
Loading...
|
||||||
</Typography>
|
</Typography>
|
||||||
</SingleElementWrapper>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
import {createContext} from "react"
|
|
||||||
|
|
||||||
import {ServerSettings} from "~/types"
|
|
||||||
|
|
||||||
export interface ServerSettingsContextType {
|
|
||||||
serverSettings: ServerSettings
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const ServerSettingsContext = createContext<ServerSettingsContextType>()
|
|
||||||
|
|
||||||
export default ServerSettingsContext
|
|
@ -4,3 +4,5 @@ export * from "./get-server-settings"
|
|||||||
export {default as getServerSettings} from "./get-server-settings"
|
export {default as getServerSettings} from "./get-server-settings"
|
||||||
export * from "./signup"
|
export * from "./signup"
|
||||||
export {default as signup} from "./signup"
|
export {default as signup} from "./signup"
|
||||||
|
export * from "./validate-email"
|
||||||
|
export {default as validateEmail} from "./validate-email"
|
||||||
|
@ -2,15 +2,15 @@ import axios from "axios"
|
|||||||
|
|
||||||
import {AuthenticationDetails} from "~/types"
|
import {AuthenticationDetails} from "~/types"
|
||||||
|
|
||||||
export interface ValidateTokenData {
|
export interface ValidateEmailData {
|
||||||
email: string
|
email: string
|
||||||
token: string
|
token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function validateToken({
|
export default async function validateEmail({
|
||||||
email,
|
email,
|
||||||
token,
|
token,
|
||||||
}: ValidateTokenData): Promise<AuthenticationDetails> {
|
}: ValidateEmailData): Promise<AuthenticationDetails> {
|
||||||
const {data} = await axios.post(
|
const {data} = await axios.post(
|
||||||
`${import.meta.env.VITE_SERVER_BASE_URL}/auth/verify-email`,
|
`${import.meta.env.VITE_SERVER_BASE_URL}/auth/verify-email`,
|
||||||
{
|
{
|
@ -1,27 +0,0 @@
|
|||||||
import React, {ReactElement} from "react"
|
|
||||||
|
|
||||||
import {Box} from "@mui/material"
|
|
||||||
|
|
||||||
export interface SingleElementWrapperProps {
|
|
||||||
children: ReactElement
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
minHeight: "100vh",
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SingleElementWrapper({
|
|
||||||
children,
|
|
||||||
}: SingleElementWrapperProps): ReactElement {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
display="flex"
|
|
||||||
flexDirection="column"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
sx={style}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
@ -8,5 +8,3 @@ export * from "./PasswordField"
|
|||||||
export {default as PasswordField} from "./PasswordField"
|
export {default as PasswordField} from "./PasswordField"
|
||||||
export * from "./SimpleForm"
|
export * from "./SimpleForm"
|
||||||
export {default as SimpleForm} from "./SimpleForm"
|
export {default as SimpleForm} from "./SimpleForm"
|
||||||
export * from "./SingleElementWrapper"
|
|
||||||
export { default as SingleElementWrapper } from "./SingleElementWrapper"
|
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
export * from "./use-server-settings"
|
|
||||||
export {default as useServerSettings} from "./use-server-settings"
|
|
@ -1,10 +0,0 @@
|
|||||||
import {useContext} from "react"
|
|
||||||
|
|
||||||
import {ServerSettings} from "~/types"
|
|
||||||
import ServerSettingsContext from "~/ServerSettingsContext"
|
|
||||||
|
|
||||||
export default function useServerSettings(): ServerSettings {
|
|
||||||
const {serverSettings} = useContext(ServerSettingsContext)
|
|
||||||
|
|
||||||
return serverSettings
|
|
||||||
}
|
|
@ -7,17 +7,20 @@ import {InputAdornment, TextField} from "@mui/material"
|
|||||||
import {MultiStepFormElement, SimpleForm} from "~/components"
|
import {MultiStepFormElement, SimpleForm} from "~/components"
|
||||||
import {signup} from "~/apis"
|
import {signup} from "~/apis"
|
||||||
import {handleErrors} from "~/utils"
|
import {handleErrors} from "~/utils"
|
||||||
import {useServerSettings} from "~/hooks"
|
import {ServerSettings} from "~/types"
|
||||||
|
|
||||||
import DetectEmailAutofillService from "./DetectEmailAutofillService"
|
import DetectEmailAutofillService from "./DetectEmailAutofillService"
|
||||||
import useSchema, {Form} from "./use-schema"
|
import useSchema, {Form} from "./use-schema"
|
||||||
|
|
||||||
interface EmailFormProps {
|
interface EmailFormProps {
|
||||||
|
serverSettings: ServerSettings
|
||||||
onSignUp: (email: string) => void
|
onSignUp: (email: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EmailForm({onSignUp}: EmailFormProps): ReactElement {
|
export default function EmailForm({
|
||||||
const serverSettings = useServerSettings()
|
onSignUp,
|
||||||
|
serverSettings,
|
||||||
|
}: EmailFormProps): ReactElement {
|
||||||
const schema = useSchema(serverSettings)
|
const schema = useSchema(serverSettings)
|
||||||
const formik = useFormik<Form>({
|
const formik = useFormik<Form>({
|
||||||
validationSchema: schema,
|
validationSchema: schema,
|
||||||
|
@ -1,30 +1,6 @@
|
|||||||
import {useLocalStorage} from "react-use"
|
import {Outlet} from "react-router-dom"
|
||||||
import React, {ReactElement} from "react"
|
import React, {ReactElement} from "react"
|
||||||
|
|
||||||
import {MultiStepForm, SingleElementWrapper} from "~/components"
|
|
||||||
import EmailForm from "~/route-widgets/root/EmailForm"
|
|
||||||
import YouGotMail from "~/route-widgets/root/YouGotMail"
|
|
||||||
|
|
||||||
export default function RootRoute(): ReactElement {
|
export default function RootRoute(): ReactElement {
|
||||||
const [email, setEmail] = useLocalStorage<string>(
|
return <Outlet />
|
||||||
"signup-form-state-email",
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
|
|
||||||
const index = email ? 1 : 0
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SingleElementWrapper>
|
|
||||||
<MultiStepForm
|
|
||||||
steps={[
|
|
||||||
<EmailForm onSignUp={setEmail} key="email" />,
|
|
||||||
<YouGotMail
|
|
||||||
domain={(email || "").split("@")[1]}
|
|
||||||
key="you_got_mail"
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
index={index}
|
|
||||||
/>
|
|
||||||
</SingleElementWrapper>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
35
src/routes/SignupRoute.tsx
Normal file
35
src/routes/SignupRoute.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {ReactElement} from "react"
|
||||||
|
import {useLocalStorage} from "react-use"
|
||||||
|
import {useLoaderData} from "react-router-dom"
|
||||||
|
|
||||||
|
import {MultiStepForm} from "~/components"
|
||||||
|
import {ServerSettings} from "~/types"
|
||||||
|
import EmailForm from "~/route-widgets/root/EmailForm"
|
||||||
|
import YouGotMail from "~/route-widgets/root/YouGotMail"
|
||||||
|
|
||||||
|
export default function SignupRoute(): ReactElement {
|
||||||
|
const serverSettings = useLoaderData() as ServerSettings
|
||||||
|
const [email, setEmail] = useLocalStorage<string>(
|
||||||
|
"signup-form-state-email",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
const index = email ? 1 : 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MultiStepForm
|
||||||
|
steps={[
|
||||||
|
<EmailForm
|
||||||
|
serverSettings={serverSettings}
|
||||||
|
onSignUp={setEmail}
|
||||||
|
key="email"
|
||||||
|
/>,
|
||||||
|
<YouGotMail
|
||||||
|
domain={(email || "").split("@")[1]}
|
||||||
|
key="you_got_mail"
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
index={index}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
49
src/routes/SingleElementRoute.tsx
Normal file
49
src/routes/SingleElementRoute.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import {ReactElement} from "react"
|
||||||
|
import {Link as RouterLink, Outlet} from "react-router-dom"
|
||||||
|
import {MdAdd, MdLogin} from "react-icons/md"
|
||||||
|
|
||||||
|
import {Box, Button, Grid} from "@mui/material"
|
||||||
|
|
||||||
|
export default function SingleElementRoute(): ReactElement {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
height="100vh"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
<Outlet />
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={2}
|
||||||
|
justifyContent="center"
|
||||||
|
marginBottom={2}
|
||||||
|
>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
component={RouterLink}
|
||||||
|
to="/signup"
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
startIcon={<MdAdd />}
|
||||||
|
>
|
||||||
|
Sign Up
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
component={RouterLink}
|
||||||
|
to="/login"
|
||||||
|
color="inherit"
|
||||||
|
size="small"
|
||||||
|
startIcon={<MdLogin />}
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
93
src/routes/VerifyEmailRoute.tsx
Normal file
93
src/routes/VerifyEmailRoute.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import * as yup from "yup"
|
||||||
|
import {useLoaderData, useParams} from "react-router-dom"
|
||||||
|
import {useAsync} from "react-use"
|
||||||
|
import {MdCancel} from "react-icons/md"
|
||||||
|
import React, {ReactElement} from "react"
|
||||||
|
|
||||||
|
import {Grid, Paper, Typography, useTheme} from "@mui/material"
|
||||||
|
|
||||||
|
import {ServerSettings} from "~/types"
|
||||||
|
import {validateEmail} from "~/apis"
|
||||||
|
|
||||||
|
const emailSchema = yup.string().email()
|
||||||
|
|
||||||
|
export default function VerifyEmailRoute(): ReactElement {
|
||||||
|
const theme = useTheme()
|
||||||
|
const {email, token} = useParams<{
|
||||||
|
email: string
|
||||||
|
token: string
|
||||||
|
}>()
|
||||||
|
const serverSettings = useLoaderData() as ServerSettings
|
||||||
|
|
||||||
|
const tokenSchema = yup
|
||||||
|
.string()
|
||||||
|
.length(serverSettings.email_verification_length)
|
||||||
|
.test("token", "Invalid token", token => {
|
||||||
|
if (!token) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check token only contains chars from `serverSettings.email_verification_chars`
|
||||||
|
const chars = serverSettings.email_verification_chars.split("")
|
||||||
|
return token.split("").every(chars.includes)
|
||||||
|
})
|
||||||
|
const {error, loading} = useAsync(async () => {
|
||||||
|
await emailSchema.validate(email)
|
||||||
|
await tokenSchema.validate(token)
|
||||||
|
|
||||||
|
await validateEmail({
|
||||||
|
token: token as string,
|
||||||
|
email: email as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}, [email, token])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={4}
|
||||||
|
padding={4}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
flexDirection="column"
|
||||||
|
>
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="h5" component="h1" align="center">
|
||||||
|
Verify your email
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
{loading ? (
|
||||||
|
<Grid item>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
component="p"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
Verifying your email...
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Grid item>
|
||||||
|
<MdCancel
|
||||||
|
size={100}
|
||||||
|
color={theme.palette.error.main}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
component="p"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
Sorry, but this token is invalid.
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
</Paper>
|
||||||
|
)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user