584 lines
18 KiB
TypeScript

import * as yup from "yup"
import {useFormik} from "formik"
import {TbCursorText} from "react-icons/tb"
import {useTranslation} from "react-i18next"
import {MdCheck, MdClear, MdOutlineChangeCircle, MdTextFormat} from "react-icons/md"
import {BsImage} from "react-icons/bs"
import {AxiosError} from "axios"
import {FaMask} from "react-icons/fa"
import AliasesPercentageAmount from "./AliasPercentageAmount"
import {
Checkbox,
Collapse,
FormControlLabel,
FormGroup,
FormHelperText,
Grid,
InputAdornment,
TextField,
Typography,
} from "@mui/material"
import {LoadingButton} from "@mui/lab"
import {useMutation} from "@tanstack/react-query"
import {AdminSettings} from "~/server-types"
import {StringPoolField, createPool} from "~/components"
import {UpdateAdminSettingsResponse, updateAdminSettings} from "~/apis"
import {useErrorSuccessSnacks} from "~/hooks"
import {queryClient} from "~/constants/react-query"
import {parseFastAPIError} from "~/utils"
import {DEFAULT_ADMIN_SETTINGS} from "~/constants/admin-settings"
import RandomAliasGenerator from "~/route-widgets/GlobalSettingsRoute/RandomAliasGenerator"
export interface SettingsFormProps {
settings: AdminSettings
queryKey: readonly string[]
}
const DEFAULT_POOLS = createPool({
abcdefghijklmnopqrstuvwxyz: "a-z",
ABCDEFGHIJKLMNOPQRSTUVWXYZ: "A-Z",
"0123456789": "0-9",
})
export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
const {t} = useTranslation(["admin-global-settings", "common"])
const {showSuccess, showError} = useErrorSuccessSnacks()
const validationSchema = yup.object().shape({
randomEmailIdMinLength: yup
.number()
.min(1)
.max(1_023)
.label(t("fields.randomEmailIdMinLength.label")),
randomEmailIdChars: yup.string().label(t("fields.randomEmailIdChars.label")),
randomEmailLengthIncreaseOnPercentage: yup
.number()
.min(0)
.max(1)
.label(t("fields.randomEmailLengthIncreaseOnPercentage.label")),
imageProxyStorageLifeTimeInHours: yup
.number()
.label(t("fields.imageProxyStorageLifeTimeInHours.label")),
customEmailSuffixLength: yup
.number()
.min(1)
.max(1_023)
.label(t("fields.customEmailSuffixLength.label")),
customEmailSuffixChars: yup.string().label(t("fields.customEmailSuffixChars.label")),
userEmailEnableDisposableEmails: yup
.boolean()
.label(t("fields.userEmailEnableDisposableEmails.label")),
userEmailEnableOtherRelays: yup
.boolean()
.label(t("fields.userEmailEnableOtherRelays.label")),
enableImageProxy: yup.boolean().label(t("fields.enableImageProxy.label")),
enableImageProxyStorage: yup.boolean().label(t("fields.enableImageProxyStorage.label")),
allowStatistics: yup.boolean().label(t("fields.allowStatistics.label")),
allowAliasDeletion: yup.boolean().label(t("fields.allowAliasDeletion.label")),
maxAliasesPerUser: yup.number().label(t("fields.maxAliasesPerUser.label")).min(0),
allowRegistrations: yup.boolean().label(t("fields.allowRegistrations.label")),
} as Record<keyof AdminSettings, any>)
const {mutateAsync} = useMutation<
UpdateAdminSettingsResponse,
AxiosError,
Partial<AdminSettings>
>(async settings => updateAdminSettings(settings), {
onError: showError,
onSuccess: ({code, detail, ...newSettings}) => {
if (code === "error:settings:global_settings_disabled") {
return
}
showSuccess(t("updatedSuccessfullyMessage"))
queryClient.setQueryData<Partial<AdminSettings>>(queryKey, newSettings)
},
})
const formik = useFormik<AdminSettings & {detail?: string}>({
validationSchema,
onSubmit: async (values, {setErrors}) => {
try {
await mutateAsync(values)
} catch (error) {
setErrors(parseFastAPIError(error as AxiosError))
}
},
initialValues: settings,
})
// Fields will either have a value or be filled from the default values.
// That means we will never have a `null` value.
return (
<form onSubmit={formik.handleSubmit}>
<Grid
container
spacing={4}
paddingX={2}
paddingY={4}
direction="column"
alignItems="center"
justifyContent="center"
>
<Grid item>
<Grid container spacing={2} direction="column">
<Grid item>
<Typography variant="h4" component="h1" align="center">
{t("title")}
</Typography>
</Grid>
<Grid item>
<Typography variant="subtitle1" component="p">
{t("description")}
</Typography>
</Grid>
</Grid>
</Grid>
<Grid item>
<Grid container spacing={3} direction="row" alignItems="start">
<Grid item xs={12}>
<TextField
key="max_aliases_per_user"
fullWidth
label={t("fields.maxAliasesPerUser.label")}
name="maxAliasesPerUser"
value={formik.values.maxAliasesPerUser}
onChange={formik.handleChange}
error={
formik.touched.maxAliasesPerUser &&
Boolean(formik.errors.maxAliasesPerUser)
}
helperText={
(formik.touched.maxAliasesPerUser &&
formik.errors.maxAliasesPerUser) ||
t("fields.maxAliasesPerUser.description")
}
type="number"
disabled={formik.isSubmitting}
inputMode="numeric"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<FaMask />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
key="random_email_id_min_length"
fullWidth
label={t("fields.randomEmailIdMinLength.label")}
name="randomEmailIdMinLength"
value={formik.values.randomEmailIdMinLength}
onChange={formik.handleChange}
error={
formik.touched.randomEmailIdMinLength &&
Boolean(formik.errors.randomEmailIdMinLength)
}
helperText={
(formik.touched.randomEmailIdMinLength &&
formik.errors.randomEmailIdMinLength) ||
t("fields.randomEmailIdMinLength.description")
}
type="number"
disabled={formik.isSubmitting}
inputMode="numeric"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<TbCursorText />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<StringPoolField
fullWidth
allowCustom
key="random_email_id_chars"
pools={DEFAULT_POOLS}
id="random_email_id_chars"
label={t("fields.randomEmailIdChars.label")}
name="randomEmailIdChars"
value={formik.values.randomEmailIdChars!}
onChange={formik.handleChange}
error={
formik.touched.randomEmailIdChars &&
Boolean(formik.errors.randomEmailIdChars)
}
helperText={
(formik.touched.randomEmailIdChars &&
formik.errors.randomEmailIdChars) ||
(t("fields.randomEmailIdChars.description") as string)
}
disabled={formik.isSubmitting}
startAdornment={
<InputAdornment position="start">
<MdTextFormat />
</InputAdornment>
}
/>
</Grid>
<Grid item xs={12} marginX="auto">
<RandomAliasGenerator
characters={formik.values.randomEmailIdChars!}
length={formik.values.randomEmailIdMinLength!}
/>
</Grid>
<Grid item xs={12}>
<TextField
key="random_email_length_increase_on_percentage"
fullWidth
label={t("fields.randomEmailLengthIncreaseOnPercentage.label")}
name="randomEmailLengthIncreaseOnPercentage"
value={formik.values.randomEmailLengthIncreaseOnPercentage}
onChange={formik.handleChange}
error={
formik.touched.randomEmailLengthIncreaseOnPercentage &&
Boolean(formik.errors.randomEmailLengthIncreaseOnPercentage)
}
helperText={
(formik.touched.randomEmailLengthIncreaseOnPercentage &&
formik.errors.randomEmailLengthIncreaseOnPercentage) ||
t("fields.randomEmailLengthIncreaseOnPercentage.description")
}
type="number"
disabled={formik.isSubmitting}
inputMode="numeric"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<MdOutlineChangeCircle />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12}>
<AliasesPercentageAmount
percentage={formik.values.randomEmailLengthIncreaseOnPercentage!}
length={formik.values.randomEmailIdMinLength!}
characters={formik.values.randomEmailIdChars!}
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField
key="custom_email_suffix_length"
fullWidth
label={t("fields.customEmailSuffixLength.label")}
name="customEmailSuffixLength"
value={formik.values.customEmailSuffixLength}
onChange={formik.handleChange}
error={
formik.touched.customEmailSuffixLength &&
Boolean(formik.errors.customEmailSuffixLength)
}
helperText={
(formik.touched.customEmailSuffixLength &&
formik.errors.customEmailSuffixLength) ||
t("fields.customEmailSuffixLength.description")
}
type="number"
disabled={formik.isSubmitting}
inputMode="numeric"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<TbCursorText />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<StringPoolField
key="custom_email_suffix_chars"
allowCustom
fullWidth
pools={DEFAULT_POOLS}
id="custom_email_suffix_chars"
label={t("fields.customEmailSuffixChars.label")}
name="customEmailSuffixChars"
value={formik.values.customEmailSuffixChars!}
onChange={formik.handleChange}
error={
formik.touched.customEmailSuffixChars &&
Boolean(formik.errors.customEmailSuffixChars)
}
helperText={
(formik.touched.customEmailSuffixChars &&
formik.errors.customEmailSuffixChars) ||
(t("fields.customEmailSuffixChars.description") as string)
}
disabled={formik.isSubmitting}
startAdornment={
<InputAdornment position="start">
<MdTextFormat />
</InputAdornment>
}
/>
</Grid>
<Grid item xs={12}>
<TextField
key="image_proxy_storage_life_time_in_hours"
fullWidth
label={t("fields.imageProxyStorageLifeTimeInHours.label")}
name="imageProxyStorageLifeTimeInHours"
value={formik.values.imageProxyStorageLifeTimeInHours}
onChange={formik.handleChange}
error={
formik.touched.imageProxyStorageLifeTimeInHours &&
Boolean(formik.errors.imageProxyStorageLifeTimeInHours)
}
helperText={
(formik.touched.imageProxyStorageLifeTimeInHours &&
formik.errors.imageProxyStorageLifeTimeInHours) ||
t("fields.imageProxyStorageLifeTimeInHours.description")
}
type="number"
disabled={formik.isSubmitting}
inputMode="numeric"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<BsImage />
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
{t("fields.imageProxyStorageLifeTimeInHours.unit", {
count:
formik.values
.imageProxyStorageLifeTimeInHours || 0,
})}
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12}>
<FormGroup key="user_email_enable_disposable_emails">
<FormControlLabel
control={
<Checkbox
checked={formik.values.userEmailEnableDisposableEmails}
onChange={formik.handleChange}
name="userEmailEnableDisposableEmails"
/>
}
disabled={formik.isSubmitting}
label={t("fields.userEmailEnableDisposableEmails.label")}
/>
<FormHelperText
error={
formik.touched.userEmailEnableDisposableEmails &&
Boolean(formik.errors.userEmailEnableDisposableEmails)
}
>
{(formik.touched.userEmailEnableDisposableEmails &&
formik.errors.userEmailEnableDisposableEmails) ||
t("fields.userEmailEnableDisposableEmails.description")}
</FormHelperText>
</FormGroup>
</Grid>
<Grid item xs={12}>
<FormGroup key="user_email_enable_other_relays">
<FormControlLabel
control={
<Checkbox
checked={formik.values.userEmailEnableOtherRelays}
onChange={formik.handleChange}
name="userEmailEnableOtherRelays"
/>
}
disabled={formik.isSubmitting}
label={t("fields.userEmailEnableOtherRelays.label")}
/>
<FormHelperText
error={
formik.touched.userEmailEnableOtherRelays &&
Boolean(formik.errors.userEmailEnableOtherRelays)
}
>
{(formik.touched.userEmailEnableOtherRelays &&
formik.errors.userEmailEnableOtherRelays) ||
t("fields.userEmailEnableOtherRelays.description")}
</FormHelperText>
</FormGroup>
</Grid>
<Grid item xs={12}>
<Grid container spacing={1}>
<Grid item>
<FormGroup key="enable_image_proxy">
<FormControlLabel
control={
<Checkbox
checked={formik.values.enableImageProxy}
onChange={formik.handleChange}
name="enableImageProxy"
/>
}
disabled={formik.isSubmitting}
label={t("fields.enableImageProxy.label")}
/>
<FormHelperText
error={
formik.touched.enableImageProxy &&
Boolean(formik.errors.enableImageProxy)
}
>
{(formik.touched.enableImageProxy &&
formik.errors.enableImageProxy) ||
t("fields.enableImageProxy.description")}
</FormHelperText>
</FormGroup>
</Grid>
<Grid item>
<Collapse in={formik.values.enableImageProxy}>
<FormGroup key="enable_image_proxy_storage">
<FormControlLabel
control={
<Checkbox
checked={
formik.values.enableImageProxyStorage
}
onChange={formik.handleChange}
name="enableImageProxyStorage"
/>
}
disabled={formik.isSubmitting}
label={t("fields.enableImageProxyStorage.label")}
/>
<FormHelperText
error={
formik.touched.enableImageProxyStorage &&
Boolean(formik.errors.enableImageProxyStorage)
}
>
{(formik.touched.enableImageProxyStorage &&
formik.errors.enableImageProxyStorage) ||
t("fields.enableImageProxyStorage.description")}
</FormHelperText>
</FormGroup>
</Collapse>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<FormGroup key="allow_statistics">
<FormControlLabel
control={
<Checkbox
checked={formik.values.allowStatistics}
onChange={formik.handleChange}
name="allowStatistics"
/>
}
disabled={formik.isSubmitting}
label={t("fields.allowStatistics.label")}
/>
<FormHelperText
error={
formik.touched.allowStatistics &&
Boolean(formik.errors.allowStatistics)
}
>
{(formik.touched.allowStatistics &&
formik.errors.allowStatistics) ||
t("fields.allowStatistics.description")}
</FormHelperText>
</FormGroup>
</Grid>
<Grid item xs={12}>
<FormGroup key="allow_alias_deletion">
<FormControlLabel
control={
<Checkbox
checked={formik.values.allowAliasDeletion}
onChange={formik.handleChange}
name="allowAliasDeletion"
/>
}
disabled={formik.isSubmitting}
label={t("fields.allowAliasDeletion.label")}
/>
<FormHelperText
error={
formik.touched.allowAliasDeletion &&
Boolean(formik.errors.allowAliasDeletion)
}
>
{(formik.touched.allowAliasDeletion &&
formik.errors.allowAliasDeletion) ||
t("fields.allowAliasDeletion.description")}
</FormHelperText>
</FormGroup>
</Grid>
<Grid item xs={12}>
<FormGroup key="allow_registrations">
<FormControlLabel
control={
<Checkbox
checked={formik.values.allowRegistrations}
onChange={formik.handleChange}
name="allowRegistrations"
/>
}
disabled={formik.isSubmitting}
label={t("fields.allowRegistrations.label")}
/>
<FormHelperText
error={
formik.touched.allowRegistrations &&
Boolean(formik.errors.allowRegistrations)
}
>
{(formik.touched.allowRegistrations &&
formik.errors.allowRegistrations) ||
t("fields.allowRegistrations.description")}
</FormHelperText>
</FormGroup>
</Grid>
</Grid>
</Grid>
<Grid item>
<Grid container justifyContent="center" gap={2}>
<Grid item>
<LoadingButton
loading={formik.isSubmitting}
variant="outlined"
type="reset"
startIcon={<MdClear />}
color="warning"
onClick={() => {
formik.setValues(DEFAULT_ADMIN_SETTINGS)
formik.submitForm()
}}
>
{t("general.resetLabel", {ns: "common"})}
</LoadingButton>
</Grid>
<Grid item>
<LoadingButton
loading={formik.isSubmitting}
variant="contained"
type="submit"
startIcon={<MdCheck />}
>
{t("general.saveLabel", {ns: "common"})}
</LoadingButton>
</Grid>
</Grid>
</Grid>
</Grid>
</form>
)
}