mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-21 08:40:32 +02:00
added SettingsRoute.tsx
This commit is contained in:
parent
b1e48544dc
commit
53589b9419
@ -14,6 +14,7 @@ import AuthenticatedRoute from "~/routes/AuthenticatedRoute"
|
||||
import CompleteAccountRoute from "~/routes/CompleteAccountRoute"
|
||||
import LoginRoute from "~/routes/LoginRoute"
|
||||
import RootRoute from "~/routes/Root"
|
||||
import SettingsRoute from "~/routes/SettingsRoute"
|
||||
import SignupRoute from "~/routes/SignupRoute"
|
||||
import VerifyEmailRoute from "~/routes/VerifyEmailRoute"
|
||||
|
||||
@ -56,6 +57,10 @@ const router = createBrowserRouter([
|
||||
path: "/aliases",
|
||||
element: <AliasesRoute />,
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
element: <SettingsRoute />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -10,6 +10,7 @@ interface AuthContextTypeBase {
|
||||
_decryptContent: (content: string) => string
|
||||
_encryptContent: (content: string) => string
|
||||
_setDecryptionPassword: (decryptionPassword: string) => void
|
||||
_updateUser: (user: ServerUser | User) => void
|
||||
}
|
||||
|
||||
interface AuthContextTypeAuthenticated {
|
||||
@ -44,6 +45,9 @@ const AuthContext = createContext<AuthContextType>({
|
||||
_setDecryptionPassword: () => {
|
||||
throw new Error("_setMasterDecryptionPassword() not implemented")
|
||||
},
|
||||
_updateUser: () => {
|
||||
throw new Error("_updateUser() not implemented")
|
||||
},
|
||||
})
|
||||
|
||||
export default AuthContext
|
||||
|
@ -89,6 +89,17 @@ export default function AuthContextProvider({
|
||||
[masterPassword, decryptContent],
|
||||
)
|
||||
|
||||
const updateUser = useCallback(
|
||||
async (newUser: ServerUser | User) => {
|
||||
if (user === null) {
|
||||
throw new Error("Can't update user when user is null.")
|
||||
}
|
||||
|
||||
setUser(newUser)
|
||||
},
|
||||
[user],
|
||||
)
|
||||
|
||||
const {mutateAsync: refresh} = useMutation<
|
||||
RefreshTokenResult,
|
||||
AxiosError,
|
||||
@ -106,6 +117,7 @@ export default function AuthContextProvider({
|
||||
_encryptContent: encryptContent,
|
||||
_decryptContent: decryptContent,
|
||||
_setDecryptionPassword: setDecryptionPassword,
|
||||
_updateUser: updateUser,
|
||||
}),
|
||||
[refresh, login, logout],
|
||||
)
|
||||
|
@ -24,3 +24,5 @@ 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"
|
||||
export * from "./update-preferences"
|
||||
export {default as updatePreferences} from "./update-preferences"
|
||||
|
28
src/apis/update-preferences.ts
Normal file
28
src/apis/update-preferences.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {
|
||||
ImageProxyFormatType,
|
||||
ProxyUserAgentType,
|
||||
SimpleDetailResponse,
|
||||
} from "~/server-types"
|
||||
import {client} from "~/constants/axios-client"
|
||||
|
||||
export interface UpdatePreferencesData {
|
||||
aliasRemoveTrackers?: boolean
|
||||
aliasCreateMailReport?: boolean
|
||||
aliasProxyImages?: boolean
|
||||
aliasImageProxyFormat?: ImageProxyFormatType
|
||||
aliasImageProxyUserAgent?: ProxyUserAgentType
|
||||
}
|
||||
|
||||
export default async function updatePreferences(
|
||||
updateData: UpdatePreferencesData,
|
||||
): Promise<SimpleDetailResponse> {
|
||||
const {data} = await client.patch(
|
||||
`${import.meta.env.VITE_SERVER_BASE_URL}/preferences`,
|
||||
updateData,
|
||||
{
|
||||
withCredentials: true,
|
||||
},
|
||||
)
|
||||
|
||||
return data
|
||||
}
|
27
src/components/ErrorSnack.tsx
Normal file
27
src/components/ErrorSnack.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React, {ReactElement, useEffect, useState} from "react"
|
||||
|
||||
import {Alert, Snackbar} from "@mui/material"
|
||||
|
||||
export interface ErrorSnackProps {
|
||||
message?: string | null | false
|
||||
}
|
||||
|
||||
export default function ErrorSnack({message}: ErrorSnackProps): ReactElement {
|
||||
const [open, setOpen] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(Boolean(message))
|
||||
}, [message])
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={5000}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<Alert severity="error" variant="filled">
|
||||
{message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
)
|
||||
}
|
29
src/components/SuccessSnack.tsx
Normal file
29
src/components/SuccessSnack.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React, {ReactElement, useEffect, useState} from "react"
|
||||
|
||||
import {Alert, Snackbar} from "@mui/material"
|
||||
|
||||
export interface SuccessSnackProps {
|
||||
message?: string | null
|
||||
}
|
||||
|
||||
export default function SuccessSnack({
|
||||
message,
|
||||
}: SuccessSnackProps): ReactElement {
|
||||
const [open, setOpen] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(Boolean(message))
|
||||
}, [message])
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={5000}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<Alert severity="success" variant="filled">
|
||||
{message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
)
|
||||
}
|
@ -12,3 +12,7 @@ export * from "./MutationStatusSnackbar"
|
||||
export {default as MutationStatusSnackbar} from "./MutationStatusSnackbar"
|
||||
export * from "./TimedButton"
|
||||
export {default as TimedButton} from "./TimedButton"
|
||||
export * from "./ErrorSnack"
|
||||
export {default as ErrorSnack} from "./ErrorSnack"
|
||||
export * from "./SuccessSnack"
|
||||
export {default as SuccessSnack} from "./SuccessSnack"
|
||||
|
359
src/route-widgets/SettingsRoute/AliasesPreferencesForm.tsx
Normal file
359
src/route-widgets/SettingsRoute/AliasesPreferencesForm.tsx
Normal file
@ -0,0 +1,359 @@
|
||||
import * as yup from "yup"
|
||||
import {AxiosError} from "axios"
|
||||
import {useFormik} from "formik"
|
||||
import {MdCheckCircle, MdImage} from "react-icons/md"
|
||||
import React, {ReactElement, useContext} from "react"
|
||||
|
||||
import {useMutation} from "@tanstack/react-query"
|
||||
import {
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
FormHelperText,
|
||||
Grid,
|
||||
InputAdornment,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material"
|
||||
import {LoadingButton} from "@mui/lab"
|
||||
|
||||
import {
|
||||
ImageProxyFormatType,
|
||||
ProxyUserAgentType,
|
||||
SimpleDetailResponse,
|
||||
} from "~/server-types"
|
||||
import {UpdatePreferencesData, updatePreferences} from "~/apis"
|
||||
import {useUser} from "~/hooks"
|
||||
import {parseFastapiError} from "~/utils"
|
||||
import {SuccessSnack} from "~/components"
|
||||
import AuthContext from "~/AuthContext/AuthContext"
|
||||
import ErrorSnack from "~/components/ErrorSnack"
|
||||
|
||||
interface Form {
|
||||
removeTrackers: boolean
|
||||
createMailReport: boolean
|
||||
proxyImages: boolean
|
||||
imageProxyFormat: ImageProxyFormatType
|
||||
imageProxyUserAgent: ProxyUserAgentType
|
||||
|
||||
detail?: string
|
||||
}
|
||||
|
||||
const SCHEMA = yup.object().shape({
|
||||
removeTrackers: yup.boolean(),
|
||||
createMailReport: yup.boolean(),
|
||||
proxyImages: yup.boolean(),
|
||||
imageProxyFormat: yup
|
||||
.mixed<ImageProxyFormatType>()
|
||||
.oneOf(Object.values(ImageProxyFormatType))
|
||||
.required(),
|
||||
imageProxyUserAgent: yup
|
||||
.mixed<ProxyUserAgentType>()
|
||||
.oneOf(Object.values(ProxyUserAgentType))
|
||||
.required(),
|
||||
})
|
||||
|
||||
const IMAGE_PROXY_FORMAT_TYPE_NAME_MAP: Record<ImageProxyFormatType, string> = {
|
||||
[ImageProxyFormatType.JPEG]: "JPEG",
|
||||
[ImageProxyFormatType.PNG]: "PNG",
|
||||
[ImageProxyFormatType.WEBP]: "WebP",
|
||||
}
|
||||
|
||||
const IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP: Record<ProxyUserAgentType, string> =
|
||||
{
|
||||
[ProxyUserAgentType.APPLE_MAIL]: "Apple Mail",
|
||||
[ProxyUserAgentType.GOOGLE_MAIL]: "Google Mail",
|
||||
[ProxyUserAgentType.CHROME]: "Chrome Browser",
|
||||
[ProxyUserAgentType.FIREFOX]: "Firefox Browser",
|
||||
[ProxyUserAgentType.OUTLOOK_MACOS]: "Outlook / MacOS",
|
||||
[ProxyUserAgentType.OUTLOOK_WINDOWS]: "Outlook / Windows",
|
||||
}
|
||||
|
||||
export default function AliasesPreferencesForm(): ReactElement {
|
||||
const {_updateUser} = useContext(AuthContext)
|
||||
const user = useUser()
|
||||
const {mutateAsync, data} = useMutation<
|
||||
SimpleDetailResponse,
|
||||
AxiosError,
|
||||
UpdatePreferencesData
|
||||
>(updatePreferences, {
|
||||
onSuccess: (_, values) => {
|
||||
const newUser = {
|
||||
...user,
|
||||
preferences: {
|
||||
...user.preferences,
|
||||
...values,
|
||||
},
|
||||
}
|
||||
|
||||
_updateUser(newUser)
|
||||
},
|
||||
})
|
||||
const formik = useFormik<Form>({
|
||||
validationSchema: SCHEMA,
|
||||
initialValues: {
|
||||
removeTrackers: user.preferences.aliasRemoveTrackers,
|
||||
createMailReport: user.preferences.aliasCreateMailReport,
|
||||
proxyImages: user.preferences.aliasProxyImages,
|
||||
imageProxyFormat: user.preferences.aliasImageProxyFormat,
|
||||
imageProxyUserAgent: user.preferences.aliasImageProxyUserAgent,
|
||||
},
|
||||
onSubmit: async (values, {setErrors}) => {
|
||||
try {
|
||||
await mutateAsync({
|
||||
aliasRemoveTrackers: values.removeTrackers,
|
||||
aliasCreateMailReport: values.createMailReport,
|
||||
aliasProxyImages: values.proxyImages,
|
||||
aliasImageProxyFormat: values.imageProxyFormat,
|
||||
aliasImageProxyUserAgent: values.imageProxyUserAgent,
|
||||
})
|
||||
} catch (error) {
|
||||
setErrors(parseFastapiError(error as AxiosError))
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container spacing={4}>
|
||||
<Grid item>
|
||||
<Typography variant="h6" component="h3">
|
||||
Aliases Preferences
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Typography variant="body1" component="p">
|
||||
Select the default behavior for your aliases. This will
|
||||
only affect aliases that do not have a custom behavior
|
||||
set.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Grid
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
container
|
||||
spacing={4}
|
||||
alignItems="flex-end"
|
||||
>
|
||||
<Grid item>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
disabled={formik.isSubmitting}
|
||||
control={
|
||||
<Checkbox
|
||||
name="removeTrackers"
|
||||
id="removeTrackers"
|
||||
checked={
|
||||
formik.values.removeTrackers
|
||||
}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
}
|
||||
labelPlacement="start"
|
||||
label="Remove Trackers"
|
||||
/>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.createMailReport &&
|
||||
formik.errors.createMailReport,
|
||||
)}
|
||||
>
|
||||
{(formik.touched.createMailReport &&
|
||||
formik.errors.createMailReport) ||
|
||||
"Remove single-pixel image trackers as well as url trackers."}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
disabled={formik.isSubmitting}
|
||||
control={
|
||||
<Checkbox
|
||||
name="createMailReport"
|
||||
id="createMailReport"
|
||||
checked={
|
||||
formik.values
|
||||
.createMailReport
|
||||
}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
}
|
||||
labelPlacement="start"
|
||||
label="Create Reports"
|
||||
/>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.createMailReport &&
|
||||
formik.errors.createMailReport,
|
||||
)}
|
||||
>
|
||||
{(formik.touched.createMailReport &&
|
||||
formik.errors.createMailReport) ||
|
||||
"Create reports of emails sent to aliases. Reports are end-to-end encrypted. Only you can access them."}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
disabled={formik.isSubmitting}
|
||||
control={
|
||||
<Checkbox
|
||||
name="proxyImages"
|
||||
id="proxyImages"
|
||||
checked={
|
||||
formik.values.proxyImages
|
||||
}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
}
|
||||
labelPlacement="start"
|
||||
label="Proxy Images"
|
||||
/>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.proxyImages &&
|
||||
formik.errors.proxyImages,
|
||||
)}
|
||||
>
|
||||
{(formik.touched.proxyImages &&
|
||||
formik.errors.proxyImages) ||
|
||||
"Proxies images in your emails through this KleckRelay instance. This adds an extra layer of privacy. Images are loaded immediately after we receive the email. They then will be stored for some time (cache time). During that time, the image will be served from us. This means the original server has no idea you have opened the mail. After the cache time, the image is loaded from the original server, but it gets proxied by us. This means the original server will not be able to access neither your IP address nor your user agent."}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<FormGroup>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<MdImage />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
name="imageProxyFormat"
|
||||
id="imageProxyFormat"
|
||||
label="Image File Type"
|
||||
value={formik.values.imageProxyFormat}
|
||||
onChange={formik.handleChange}
|
||||
disabled={formik.isSubmitting}
|
||||
error={
|
||||
formik.touched.imageProxyFormat &&
|
||||
Boolean(
|
||||
formik.errors.imageProxyFormat,
|
||||
)
|
||||
}
|
||||
helperText={
|
||||
formik.touched.imageProxyFormat &&
|
||||
formik.errors.imageProxyFormat
|
||||
}
|
||||
>
|
||||
{Object.entries(
|
||||
ImageProxyFormatType,
|
||||
).map(([key, value]) => (
|
||||
<MenuItem key={key} value={value}>
|
||||
{
|
||||
IMAGE_PROXY_FORMAT_TYPE_NAME_MAP[
|
||||
value
|
||||
] as string
|
||||
}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched.imageProxyFormat &&
|
||||
formik.errors.imageProxyFormat,
|
||||
)}
|
||||
>
|
||||
{formik.touched.imageProxyFormat &&
|
||||
formik.errors.imageProxyFormat}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<FormGroup>
|
||||
<TextField
|
||||
fullWidth
|
||||
select
|
||||
name="imageProxyUserAgent"
|
||||
id="imageProxyUserAgent"
|
||||
label="Image Proxy User Agent"
|
||||
value={
|
||||
formik.values.imageProxyUserAgent
|
||||
}
|
||||
onChange={formik.handleChange}
|
||||
disabled={formik.isSubmitting}
|
||||
error={
|
||||
formik.touched
|
||||
.imageProxyUserAgent &&
|
||||
Boolean(
|
||||
formik.errors
|
||||
.imageProxyUserAgent,
|
||||
)
|
||||
}
|
||||
helperText={
|
||||
formik.touched
|
||||
.imageProxyUserAgent &&
|
||||
formik.errors.imageProxyUserAgent
|
||||
}
|
||||
>
|
||||
{Object.entries(ProxyUserAgentType).map(
|
||||
([key, value]) => (
|
||||
<MenuItem
|
||||
key={key}
|
||||
value={value}
|
||||
>
|
||||
{
|
||||
IMAGE_PROXY_USER_AGENT_TYPE_NAME_MAP[
|
||||
value
|
||||
] as string
|
||||
}
|
||||
</MenuItem>
|
||||
),
|
||||
)}
|
||||
</TextField>
|
||||
<FormHelperText
|
||||
error={Boolean(
|
||||
formik.touched
|
||||
.imageProxyUserAgent &&
|
||||
formik.errors
|
||||
.imageProxyUserAgent,
|
||||
)}
|
||||
>
|
||||
{(formik.touched.imageProxyUserAgent &&
|
||||
formik.errors
|
||||
.imageProxyUserAgent) ||
|
||||
"An User Agent is a identifier each browser and email client sends when retrieving files, such as images. You can specify here what user agent you would like to be used by the proxy. User Agents are kept up-to-date."}
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<LoadingButton
|
||||
loading={formik.isSubmitting}
|
||||
variant="contained"
|
||||
type="submit"
|
||||
startIcon={<MdCheckCircle />}
|
||||
>
|
||||
Save Preferences
|
||||
</LoadingButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<ErrorSnack message={formik.errors.detail} />
|
||||
<SuccessSnack message={data?.detail} />
|
||||
</>
|
||||
)
|
||||
}
|
@ -1,7 +1,15 @@
|
||||
import {ReactElement} from "react"
|
||||
import {Outlet} from "react-router-dom"
|
||||
|
||||
import {Box, Container, List, ListItem, Paper, useTheme} from "@mui/material"
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Grid,
|
||||
List,
|
||||
ListItem,
|
||||
Paper,
|
||||
useTheme,
|
||||
} from "@mui/material"
|
||||
|
||||
import {useUser} from "~/hooks"
|
||||
import NavigationButton, {
|
||||
@ -32,32 +40,39 @@ export default function AuthenticatedRoute(): ReactElement {
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
<Grid
|
||||
container
|
||||
justifyContent="center"
|
||||
alignItems="flex-start"
|
||||
>
|
||||
<Box
|
||||
bgcolor={theme.palette.background.paper}
|
||||
component="nav"
|
||||
>
|
||||
<List>
|
||||
{sections.map(key => (
|
||||
<ListItem key={key}>
|
||||
<NavigationButton
|
||||
section={NavigationSection[key]}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
<Paper>
|
||||
<Box padding={4} maxHeight="80vh" overflow="scroll">
|
||||
<Outlet />
|
||||
<Grid item xs={12} sm={4}>
|
||||
<Box
|
||||
bgcolor={theme.palette.background.paper}
|
||||
component="nav"
|
||||
>
|
||||
<List>
|
||||
{sections.map(key => (
|
||||
<ListItem key={key}>
|
||||
<NavigationButton
|
||||
section={NavigationSection[key]}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={8}>
|
||||
<Paper>
|
||||
<Box
|
||||
padding={4}
|
||||
maxHeight="80vh"
|
||||
overflow="scroll"
|
||||
>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
</Box>
|
||||
|
20
src/routes/SettingsRoute.tsx
Normal file
20
src/routes/SettingsRoute.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React, {ReactElement} from "react"
|
||||
|
||||
import {Grid, Typography} from "@mui/material"
|
||||
|
||||
import AliasesPreferencesForm from "~/route-widgets/SettingsRoute/AliasesPreferencesForm"
|
||||
|
||||
export default function SettingsRoute(): ReactElement {
|
||||
return (
|
||||
<Grid container spacing={4}>
|
||||
<Grid item>
|
||||
<Typography variant="h5" component="h2">
|
||||
Settings
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<AliasesPreferencesForm />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user