mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-21 08:40:32 +02:00
added apis; added widgets
This commit is contained in:
parent
550baf1b66
commit
55fddaf0d9
@ -16,6 +16,7 @@
|
|||||||
"openpgp": "^5.5.0",
|
"openpgp": "^5.5.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-icons": "^4.4.0",
|
||||||
"react-router-dom": "^6.4.2",
|
"react-router-dom": "^6.4.2",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
"ua-parser-js": "^1.0.2",
|
"ua-parser-js": "^1.0.2",
|
||||||
@ -25,6 +26,7 @@
|
|||||||
"@types/openpgp": "^4.4.18",
|
"@types/openpgp": "^4.4.18",
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
|
"@types/react-icons": "^3.0.0",
|
||||||
"@types/react-router": "^5.1.19",
|
"@types/react-router": "^5.1.19",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
|
9
src/apis/check-is-domain-disposable.ts
Normal file
9
src/apis/check-is-domain-disposable.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
|
||||||
|
export default async function checkIsDomainDisposable(
|
||||||
|
domain: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const {data} = await axios.get(`https://api.mailcheck.ai/domain/${domain}`)
|
||||||
|
|
||||||
|
return !data.mx || data.disposable
|
||||||
|
}
|
8
src/apis/get-server-settings.ts
Normal file
8
src/apis/get-server-settings.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
import {ServerSettings} from "~/types";
|
||||||
|
|
||||||
|
export default async function getServerSettings(): Promise<ServerSettings> {
|
||||||
|
return (
|
||||||
|
await axios.get(`${process.env.NEXT_PUBLIC_SERVER_BASE_URL}/settings`)
|
||||||
|
).data
|
||||||
|
}
|
6
src/apis/index.ts
Normal file
6
src/apis/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export * from "./check-is-domain-disposable"
|
||||||
|
export {default as checkIsDomainDisposable} from "./check-is-domain-disposable"
|
||||||
|
export * from "./get-server-settings"
|
||||||
|
export {default as getServerSettings} from "./get-server-settings"
|
||||||
|
export * from "./signup"
|
||||||
|
export {default as signup} from "./signup"
|
1
src/apis/login.ts
Normal file
1
src/apis/login.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function login() {}
|
16
src/apis/signup.ts
Normal file
16
src/apis/signup.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import axios from "axios"
|
||||||
|
|
||||||
|
export interface SignupResult {
|
||||||
|
normalized_email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function signup(email: string): Promise<SignupResult> {
|
||||||
|
const {data} = await axios.post(
|
||||||
|
`${process.env.NEXT_PUBLIC_SERVER_BASE_URL}/auth/signup`,
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
22
src/apis/validate-token.ts
Normal file
22
src/apis/validate-token.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import {AuthenticationDetails} from "~/types"
|
||||||
|
import axios from "axios"
|
||||||
|
|
||||||
|
export interface ValidateTokenData {
|
||||||
|
email: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function validateToken({
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
}: ValidateTokenData): Promise<AuthenticationDetails> {
|
||||||
|
const {data} = await axios.post(
|
||||||
|
`${process.env.NEXT_PUBLIC_SERVER_BASE_URL}/auth/verify-email`,
|
||||||
|
{
|
||||||
|
email: email,
|
||||||
|
token: token,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import {forwardRef, ReactElement, useEffect, useRef, useState} from "react"
|
import React, {forwardRef, ReactElement, useEffect, useRef, useState} from "react"
|
||||||
import {Paper} from "@mui/material"
|
import {Paper} from "@mui/material"
|
||||||
import whenElementHasBounds from "~/utils/when-element-has-bounds"
|
import {whenElementHasBounds} from "~/utils"
|
||||||
|
|
||||||
export interface MultiStepFormProps {
|
export interface MultiStepFormProps {
|
||||||
steps: (() => ReactElement)[]
|
steps: (() => ReactElement)[]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {Box, Container} from "@mui/material"
|
import {Box, Container} from "@mui/material"
|
||||||
import {ReactElement} from "react"
|
import React, {ReactElement} from "react"
|
||||||
|
|
||||||
export interface MultiStepFormElementProps {
|
export interface MultiStepFormElementProps {
|
||||||
children: ReactElement
|
children: ReactElement
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {ReactElement} from "react"
|
import {ReactElement} from "react"
|
||||||
import UAParser from "ua-parser-js"
|
import UAParser from "ua-parser-js"
|
||||||
import {Button} from "@mui/material"
|
import {Button} from "@mui/material"
|
||||||
import APP_LINK_MAP from "utils/app-url-links"
|
import {APP_LINK_MAP} from "utils"
|
||||||
import {IoMdMailOpen} from "react-icons/io"
|
import {IoMdMailOpen} from "react-icons/io"
|
||||||
|
|
||||||
export interface OpenMailButtonProps {
|
export interface OpenMailButtonProps {
|
||||||
|
12
src/components/index.ts
Normal file
12
src/components/index.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export * from "./MultiStepForm"
|
||||||
|
export { default as MultiStepForm } from "./MultiStepForm"
|
||||||
|
export * from "./MultiStepFormElement"
|
||||||
|
export { default as MultiStepFormElement } from "./MultiStepFormElement"
|
||||||
|
export * from "./OpenMailButton"
|
||||||
|
export { default as OpenMailButton } from "./OpenMailButton"
|
||||||
|
export * from "./PasswordField"
|
||||||
|
export { default as PasswordField } from "./PasswordField"
|
||||||
|
export * from "./SimpleForm"
|
||||||
|
export { default as SimpleForm } from "./SimpleForm"
|
||||||
|
export * from "./SingleElementWrapper"
|
||||||
|
export { default as SingleElementWrapper } from "./SingleElementWrapper"
|
@ -1,15 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import {
|
import {createBrowserRouter, RouterProvider,} from "react-router-dom";
|
||||||
createBrowserRouter,
|
import RootRoute from "~/routes/Root";
|
||||||
RouterProvider,
|
|
||||||
Route,
|
|
||||||
} from "react-router-dom";
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <div>Hello world!</div>,
|
element: <RootRoute />,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
140
src/route-widgets/root/EmailForm/DetectEmailAutofillService.tsx
Normal file
140
src/route-widgets/root/EmailForm/DetectEmailAutofillService.tsx
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import {ReactElement, useCallback, useEffect, useRef, useState} from "react"
|
||||||
|
import {
|
||||||
|
Alert,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
Grid,
|
||||||
|
} from "@mui/material"
|
||||||
|
import {MdCheck} from "react-icons/md"
|
||||||
|
import useSessionStorage from "hooks/use-session-storage"
|
||||||
|
|
||||||
|
export interface DetectEmailAutofillServiceProps {
|
||||||
|
domains: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AliasType {
|
||||||
|
DuckDuckGo = "duck.com",
|
||||||
|
SimpleLogin = "simplelogin.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPE_NAME_MAP: Record<AliasType, string> = {
|
||||||
|
[AliasType.DuckDuckGo]: "DuckDuckGo's Email Tracking Protection",
|
||||||
|
[AliasType.SimpleLogin]: "SimpleLogin",
|
||||||
|
}
|
||||||
|
|
||||||
|
const STORAGE_KEY = "has-shown-email-autofill-service"
|
||||||
|
|
||||||
|
export default function DetectEmailAutofillService({
|
||||||
|
domains,
|
||||||
|
}: DetectEmailAutofillServiceProps): ReactElement {
|
||||||
|
const $hasDetected = useRef<boolean>(false)
|
||||||
|
|
||||||
|
const [type, setType] = useState<AliasType | null>(null)
|
||||||
|
const [hasShownModal, setHasShownModal] = useSessionStorage<boolean>(
|
||||||
|
STORAGE_KEY,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleFound = useCallback(
|
||||||
|
(type: AliasType) => {
|
||||||
|
if (domains.includes(type)) {
|
||||||
|
if (hasShownModal) {
|
||||||
|
setType(type)
|
||||||
|
|
||||||
|
setHasShownModal(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[domains.length, hasShownModal],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkDuckDuckGo = () => {
|
||||||
|
const $element = document.querySelector("body > ddg-autofill")
|
||||||
|
|
||||||
|
if ($element) {
|
||||||
|
$hasDetected.current = true
|
||||||
|
handleFound(AliasType.DuckDuckGo)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const checkSimpleLogin = () => {
|
||||||
|
const $element = document.querySelector(
|
||||||
|
"body > div.sl-button-wrapper",
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
$element &&
|
||||||
|
$element.children[0].nodeName === "DIV" &&
|
||||||
|
$element.children[0].className === "sl-button"
|
||||||
|
) {
|
||||||
|
$hasDetected.current = true
|
||||||
|
handleFound(AliasType.SimpleLogin)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkDuckDuckGo() || checkSimpleLogin()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
if ($hasDetected.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDuckDuckGo()
|
||||||
|
checkSimpleLogin()
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(document.body, {subtree: false, childList: true})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [handleFound])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={type !== null} onClose={() => setType(null)}>
|
||||||
|
<DialogTitle>Email relay service detected</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Grid container spacing={2} justifyContent="center">
|
||||||
|
<Grid item>
|
||||||
|
<DialogContentText>
|
||||||
|
We detected that you are using an email relay
|
||||||
|
service to sign up. This KleckRelay instance does
|
||||||
|
not support relaying to another email relay service.
|
||||||
|
You can either choose a different instance or sign
|
||||||
|
up with a different email address.
|
||||||
|
</DialogContentText>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<DialogContentText>
|
||||||
|
Detected email relay:
|
||||||
|
</DialogContentText>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Alert severity="info">{TYPE_NAME_MAP[type!]}</Alert>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
autoFocus
|
||||||
|
startIcon={<MdCheck />}
|
||||||
|
onClick={() => setType(null)}
|
||||||
|
>
|
||||||
|
Got it
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
82
src/route-widgets/root/EmailForm/index.tsx
Normal file
82
src/route-widgets/root/EmailForm/index.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import {ReactElement} from "react"
|
||||||
|
import {useFormik} from "formik"
|
||||||
|
import useSchema, {Form} from "./use-schema"
|
||||||
|
import {ServerSettings} from "apis/get-server-settings"
|
||||||
|
import signup from "apis/signup"
|
||||||
|
import {MdEmail} from "react-icons/md"
|
||||||
|
import {InputAdornment, TextField} from "@mui/material"
|
||||||
|
import DetectEmailAutofillService from "./DetectEmailAutofillService"
|
||||||
|
import handleErrors from "utils/handle-errors"
|
||||||
|
import {MultiStepFormElement, SimpleForm} from "components"
|
||||||
|
|
||||||
|
interface EmailFormProps {
|
||||||
|
serverSettings: ServerSettings
|
||||||
|
onSignUp: (email: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EmailForm({
|
||||||
|
serverSettings,
|
||||||
|
onSignUp,
|
||||||
|
}: EmailFormProps): ReactElement {
|
||||||
|
const schema = useSchema(serverSettings)
|
||||||
|
const formik = useFormik<Form>({
|
||||||
|
validationSchema: schema,
|
||||||
|
initialValues: {
|
||||||
|
email: "",
|
||||||
|
},
|
||||||
|
onSubmit: (values, {setErrors}) =>
|
||||||
|
handleErrors(
|
||||||
|
values.email,
|
||||||
|
setErrors,
|
||||||
|
)(signup).then(({normalized_email}) => onSignUp(normalized_email)),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MultiStepFormElement>
|
||||||
|
<form onSubmit={formik.handleSubmit}>
|
||||||
|
<SimpleForm
|
||||||
|
title="Sign up"
|
||||||
|
description="We only need your email and you are ready to go!"
|
||||||
|
continueActionLabel="Sign up"
|
||||||
|
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>
|
||||||
|
{!serverSettings.other_relays_enabled && (
|
||||||
|
<DetectEmailAutofillService
|
||||||
|
domains={serverSettings.other_relay_domains}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
43
src/route-widgets/root/EmailForm/use-schema.ts
Normal file
43
src/route-widgets/root/EmailForm/use-schema.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import * as yup from "yup"
|
||||||
|
import {ServerSettings} from "apis/get-server-settings"
|
||||||
|
import checkIsDomainDisposable from "apis/check-is-domain-disposable"
|
||||||
|
|
||||||
|
export interface Form {
|
||||||
|
email: string
|
||||||
|
detail?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useSchema(
|
||||||
|
serverSettings: ServerSettings,
|
||||||
|
): yup.BaseSchema {
|
||||||
|
return yup.object().shape({
|
||||||
|
email: yup
|
||||||
|
.string()
|
||||||
|
.email()
|
||||||
|
.required()
|
||||||
|
.test(
|
||||||
|
"notDisposable",
|
||||||
|
"Disposable email addresses are not allowed",
|
||||||
|
async (value, context) => {
|
||||||
|
if (serverSettings.disposable_emails_enabled) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await yup.string().email().validate(value, {
|
||||||
|
strict: true,
|
||||||
|
})
|
||||||
|
const isDisposable = await checkIsDomainDisposable(
|
||||||
|
value!.split("@")[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
return !isDisposable
|
||||||
|
} catch ({message}) {
|
||||||
|
// @ts-ignore
|
||||||
|
context.createError({message})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
85
src/route-widgets/root/GenerateEmailReportsForm.tsx
Normal file
85
src/route-widgets/root/GenerateEmailReportsForm.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import {ReactElement} from "react"
|
||||||
|
import {Box, Button, Grid, Typography} from "@mui/material"
|
||||||
|
import {FaLongArrowAltRight} from "react-icons/fa"
|
||||||
|
import {TiCancel} from "react-icons/ti"
|
||||||
|
|
||||||
|
export interface GenerateEmailReportsFormProps {
|
||||||
|
onYes: () => void
|
||||||
|
onNo: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GenerateEmailReportsForm({
|
||||||
|
onNo,
|
||||||
|
onYes,
|
||||||
|
}: GenerateEmailReportsFormProps): ReactElement {
|
||||||
|
return (
|
||||||
|
<Box width="80vw">
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="column"
|
||||||
|
spacing={4}
|
||||||
|
paddingX={2}
|
||||||
|
paddingTop={4}
|
||||||
|
paddingBottom={1}
|
||||||
|
alignItems="end"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Grid item>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="column"
|
||||||
|
spacing={4}
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Grid item>
|
||||||
|
<Grid container spacing={2} direction="column">
|
||||||
|
<Grid item>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
component="h2"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
Generate Email Reports?
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
component="p"
|
||||||
|
>
|
||||||
|
Would you like to create fully encrypted
|
||||||
|
email reports for your mails? Only you
|
||||||
|
will be able to access it. Not even we
|
||||||
|
can decrypt it.
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Grid container spacing={2} direction="row">
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
startIcon={<TiCancel />}
|
||||||
|
color="secondary"
|
||||||
|
onClick={onNo}
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Button
|
||||||
|
endIcon={<FaLongArrowAltRight />}
|
||||||
|
color="primary"
|
||||||
|
onClick={onYes}
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
168
src/route-widgets/root/PasswordForm.tsx
Normal file
168
src/route-widgets/root/PasswordForm.tsx
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import {ReactElement, useMemo} from "react"
|
||||||
|
import * as yup from "yup"
|
||||||
|
import {useFormik} from "formik"
|
||||||
|
import {Box, Grid, InputAdornment, Typography} from "@mui/material"
|
||||||
|
import {MdCheckCircle, MdChevronRight, MdLock} from "react-icons/md"
|
||||||
|
import {LoadingButton} from "@mui/lab"
|
||||||
|
import {PasswordField} from "components"
|
||||||
|
import handleErrors from "utils/handle-errors"
|
||||||
|
import {generateKey} from "openpgp"
|
||||||
|
import {isDev} from "constants/values"
|
||||||
|
import encryptString from "utils/encrypt-string"
|
||||||
|
|
||||||
|
export interface PasswordFormProps {
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Form {
|
||||||
|
password: string
|
||||||
|
passwordConfirmation: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = yup.object().shape({
|
||||||
|
password: yup.string().required(),
|
||||||
|
passwordConfirmation: yup
|
||||||
|
.string()
|
||||||
|
.required()
|
||||||
|
.oneOf([yup.ref("password"), null], "Passwords must match"),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function PasswordForm({email}: PasswordFormProps): ReactElement {
|
||||||
|
const awaitGenerateKey = useMemo(
|
||||||
|
() =>
|
||||||
|
generateKey({
|
||||||
|
type: "rsa",
|
||||||
|
format: "armored",
|
||||||
|
curve: "curve25519",
|
||||||
|
userIDs: [{name: "John Smith", email: "john@example.com"}],
|
||||||
|
passphrase: "",
|
||||||
|
rsaBits: isDev ? 2048 : 4096,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
const formik = useFormik<Form>({
|
||||||
|
validationSchema: schema,
|
||||||
|
initialValues: {
|
||||||
|
password: "",
|
||||||
|
passwordConfirmation: "",
|
||||||
|
},
|
||||||
|
onSubmit: (values, {setErrors}) =>
|
||||||
|
handleErrors(
|
||||||
|
values,
|
||||||
|
setErrors,
|
||||||
|
)(async () => {
|
||||||
|
const keyPair = await awaitGenerateKey
|
||||||
|
|
||||||
|
const encryptedPrivateKey = encryptString(
|
||||||
|
keyPair.privateKey,
|
||||||
|
`${values.password}-${email}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(encryptedPrivateKey)
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box width="80vw">
|
||||||
|
<form onSubmit={formik.handleSubmit}>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={4}
|
||||||
|
paddingX={2}
|
||||||
|
paddingY={4}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Grid item>
|
||||||
|
<Grid container spacing={2} direction="column">
|
||||||
|
<Grid item>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
component="h2"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
Set up your password
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="subtitle1" component="p">
|
||||||
|
Please enter a safe password so that we can
|
||||||
|
encrypt your data.
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Grid container spacing={2} justifyContent="center">
|
||||||
|
<Grid item>
|
||||||
|
<PasswordField
|
||||||
|
fullWidth
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
value={formik.values.password}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
error={
|
||||||
|
formik.touched.password &&
|
||||||
|
Boolean(formik.errors.password)
|
||||||
|
}
|
||||||
|
helperText={
|
||||||
|
formik.touched.password &&
|
||||||
|
formik.errors.password
|
||||||
|
}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<MdLock />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<PasswordField
|
||||||
|
fullWidth
|
||||||
|
id="passwordConfirmation"
|
||||||
|
name="passwordConfirmation"
|
||||||
|
label="Confirm Password"
|
||||||
|
value={formik.values.passwordConfirmation}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
error={
|
||||||
|
formik.touched.passwordConfirmation &&
|
||||||
|
Boolean(
|
||||||
|
formik.errors.passwordConfirmation,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
helperText={
|
||||||
|
formik.touched.passwordConfirmation &&
|
||||||
|
formik.errors.passwordConfirmation
|
||||||
|
}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<MdCheckCircle />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<LoadingButton
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
loading={formik.isSubmitting}
|
||||||
|
startIcon={<MdChevronRight />}
|
||||||
|
>
|
||||||
|
Continue
|
||||||
|
</LoadingButton>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</form>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
39
src/route-widgets/root/YouGotMail.tsx
Normal file
39
src/route-widgets/root/YouGotMail.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {ReactElement} from "react"
|
||||||
|
import {Grid, Typography} from "@mui/material"
|
||||||
|
import {MultiStepFormElement, OpenMailButton} from "~/components"
|
||||||
|
|
||||||
|
export interface YouGotMailProps {
|
||||||
|
domain: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function YouGotMail({domain}: YouGotMailProps): ReactElement {
|
||||||
|
return (
|
||||||
|
<MultiStepFormElement>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="column"
|
||||||
|
spacing={4}
|
||||||
|
paddingX={2}
|
||||||
|
paddingY={4}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
>
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="h6" component="h2" align="center">
|
||||||
|
You got mail!
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="subtitle1" component="p">
|
||||||
|
We sent you an email with a link to confirm your email
|
||||||
|
address. Please check your inbox and click on the link
|
||||||
|
to continue.
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<OpenMailButton domain={domain} />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</MultiStepFormElement>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
import {ReactElement} from "react";
|
||||||
|
import SingleElementWrapper from "~/components/SingleElementWrapper";
|
||||||
|
import MultiStepForm from "~/components/MultiStepForm";
|
||||||
|
import EmailForm from "~/route-widgets/root/EmailForm";
|
||||||
|
import YouGotMail from "~/route-widgets/root/YouGotMail";
|
||||||
|
|
||||||
|
export default function RootRoute(): ReactElement {
|
||||||
|
return (
|
||||||
|
<SingleElementWrapper>
|
||||||
|
<MultiStepForm
|
||||||
|
steps={[
|
||||||
|
() => (
|
||||||
|
<EmailForm
|
||||||
|
serverSettings={{}}
|
||||||
|
onSignUp={() => null}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
() => <YouGotMail domain={""} />,
|
||||||
|
]}
|
||||||
|
index={0}
|
||||||
|
/>
|
||||||
|
</SingleElementWrapper>
|
||||||
|
);
|
||||||
|
}
|
49
src/types.d.ts
vendored
Normal file
49
src/types.d.ts
vendored
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
export enum ImageProxyFormatType {
|
||||||
|
WEBP = "webp",
|
||||||
|
PNG = "png",
|
||||||
|
JPEG = "jpeg",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProxyUserAgentType {
|
||||||
|
APPLE_MAIL = "apple-mail",
|
||||||
|
GOOGLE_MAIL = "google-mail",
|
||||||
|
OUTLOOK_WINDOWS = "outlook-windows",
|
||||||
|
OUTLOOK_MACOS = "outlook-macos",
|
||||||
|
FIREFOX = "firefox",
|
||||||
|
CHROME = "chrome",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string
|
||||||
|
createdAt: Date
|
||||||
|
email: {
|
||||||
|
address: string
|
||||||
|
isVerified: boolean
|
||||||
|
}
|
||||||
|
preferences: {
|
||||||
|
aliasRemoveTrackers: boolean
|
||||||
|
aliasCreateMailReport: boolean
|
||||||
|
aliasProxyImages: boolean
|
||||||
|
aliasImageProxyFormat: ImageProxyFormatType
|
||||||
|
aliasImageProxyUserAgent: ProxyUserAgentType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthenticationDetails {
|
||||||
|
access_token: string
|
||||||
|
refresh_token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServerSettings {
|
||||||
|
mail_domain: string
|
||||||
|
random_email_id_min_length: number
|
||||||
|
random_email_id_chars: string
|
||||||
|
image_proxy_enabled: boolean
|
||||||
|
image_proxy_life_time: number
|
||||||
|
disposable_emails_enabled: boolean
|
||||||
|
other_relays_enabled: boolean
|
||||||
|
other_relay_domains: Array<string>
|
||||||
|
email_verification_chars: string
|
||||||
|
email_verification_length: number
|
||||||
|
}
|
||||||
|
|
10
src/utils/index.ts
Normal file
10
src/utils/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export * from "./app-url-links"
|
||||||
|
export {default as APP_LINK_MAP} from "./app-url-links"
|
||||||
|
export * from "./encrypt-string"
|
||||||
|
export { default as encryptString } from "./encrypt-string"
|
||||||
|
export * from "./handle-errors"
|
||||||
|
export {default as handleErrors} from "./handle-errors"
|
||||||
|
export * from "./parse-fastapi-error"
|
||||||
|
export {default as parseFastapiError} from "./parse-fastapi-error"
|
||||||
|
export * from "./when-element-has-bounds"
|
||||||
|
export {default as whenElementHasBounds} from "./when-element-has-bounds"
|
12
yarn.lock
12
yarn.lock
@ -561,6 +561,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react-icons@^3.0.0":
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-icons/-/react-icons-3.0.0.tgz#27ca2823a6add881d06a371bfff093afc1b9c829"
|
||||||
|
integrity sha512-Vefs6LkLqF61vfV7AiAqls+vpR94q67gunhMueDznG+msAkrYgRxl7gYjNem/kZ+as2l2mNChmF1jRZzzQQtMg==
|
||||||
|
dependencies:
|
||||||
|
react-icons "*"
|
||||||
|
|
||||||
"@types/react-is@^16.7.1 || ^17.0.0":
|
"@types/react-is@^16.7.1 || ^17.0.0":
|
||||||
version "17.0.3"
|
version "17.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a"
|
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a"
|
||||||
@ -2201,6 +2208,11 @@ react-fast-compare@^2.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||||
|
|
||||||
|
react-icons@*, react-icons@^4.4.0:
|
||||||
|
version "4.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.4.0.tgz#a13a8a20c254854e1ec9aecef28a95cdf24ef703"
|
||||||
|
integrity sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==
|
||||||
|
|
||||||
react-is@^16.13.1, react-is@^16.7.0:
|
react-is@^16.13.1, react-is@^16.7.0:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user