Merge pull request #9 from Myzel394/update-admin-page

Update admin page
This commit is contained in:
Myzel394 2023-02-20 18:23:16 +01:00 committed by GitHub
commit 31f974368a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 114 additions and 70 deletions

View File

@ -2,7 +2,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>Vite + React + TS</title>

View File

@ -378,7 +378,7 @@
}, },
"enableImageProxy": { "enableImageProxy": {
"label": "Enable image proxy", "label": "Enable image proxy",
"description": "If enabled, images will be stored on the server and forwarded to the user. This is useful if you want to prevent the user from seeing the IP address of the server. This will only affect new images." "description": "If enabled, images will be stored on the server and forwarded to the user. This makes email tracking nearly impossible as every message will be marked as read instantly, which is obviously not true as a user is typically not able to view an email in just a few seconds after it has been sent. This will only affect new images."
}, },
"userEmailEnableDisposableEmails": { "userEmailEnableDisposableEmails": {
"label": "Enable disposable emails for new accounts", "label": "Enable disposable emails for new accounts",
@ -395,6 +395,10 @@
"allowAliasDeletion": { "allowAliasDeletion": {
"label": "Allow alias deletion", "label": "Allow alias deletion",
"description": "If enabled, users will be able to delete their aliases." "description": "If enabled, users will be able to delete their aliases."
},
"maxAliasesPerUser": {
"label": "Maximum aliases per user",
"description": "The maximum number of aliases a user can create. 0 means unlimited. Existing aliases will not be affected."
} }
} }
}, },

View File

@ -1,7 +1,12 @@
import {ServerCronReport} from "~/server-types" import {ServerCronReport} from "~/server-types"
import {client} from "~/constants/axios-client" import {client} from "~/constants/axios-client"
export default async function getLatestCronReport(): Promise<ServerCronReport> { export type GetLatestCronReportResponse = ServerCronReport & {
detail: string
code: "error:cron_report:no_reports_found"
}
export default async function getLatestCronReport(): Promise<GetLatestCronReportResponse> {
const {data} = await client.get( const {data} = await client.get(
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/cron-report/latest/`, `${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/cron-report/latest/`,
{ {

View File

@ -10,13 +10,14 @@ import LoadingData from "./LoadingData"
export interface QueryResultProps<TQueryFnData = unknown, TError = AxiosError> { export interface QueryResultProps<TQueryFnData = unknown, TError = AxiosError> {
query: UseQueryResult<TQueryFnData, TError> query: UseQueryResult<TQueryFnData, TError>
children: (data: TQueryFnData) => ReactElement children: (data: TQueryFnData) => ReactElement | null
} }
export default function QueryResult<TQueryFnData, TError = AxiosError>({ export default function QueryResult<
query, TQueryFnData,
children: render, TError = AxiosError,
}: QueryResultProps<TQueryFnData, TError>): ReactElement { ReturnType = ReactElement | null,
>({query, children: render}: QueryResultProps<TQueryFnData, TError>): ReactElement | null {
if (query.data !== undefined) { if (query.data !== undefined) {
return render(query.data) return render(query.data)
} }

View File

@ -12,4 +12,5 @@ export const DEFAULT_ADMIN_SETTINGS: AdminSettings = {
enableImageProxy: true, enableImageProxy: true,
allowStatistics: true, allowStatistics: true,
allowAliasDeletion: false, allowAliasDeletion: false,
maxAliasesPerUser: 0,
} }

View File

@ -3,26 +3,31 @@ import {AxiosError} from "axios"
import {useLoaderData} from "react-router-dom" import {useLoaderData} from "react-router-dom"
import {useTranslation} from "react-i18next" import {useTranslation} from "react-i18next"
import format from "date-fns/format" import format from "date-fns/format"
import formatRelative from "date-fns/formatRelative"
import subDays from "date-fns/subDays" import subDays from "date-fns/subDays"
import {useQuery} from "@tanstack/react-query" import {useQuery} from "@tanstack/react-query"
import {Alert} from "@mui/material" import {Alert} from "@mui/material"
import {WithEncryptionRequired} from "~/hocs"
import {CronReport, ServerSettings} from "~/server-types" import {CronReport, ServerSettings} from "~/server-types"
import {getLatestCronReport} from "~/apis" import {getLatestCronReport} from "~/apis"
import {AuthContext, QueryResult} from "~/components" import {AuthContext, QueryResult} from "~/components"
import decryptCronReportData from "~/apis/helpers/decrypt-cron-report-data" import decryptCronReportData from "~/apis/helpers/decrypt-cron-report-data"
import formatRelative from "date-fns/formatRelative"
const MAX_REPORT_DAY_THRESHOLD = 5 const MAX_REPORT_DAY_THRESHOLD = 5
export default function ServerStatus(): ReactElement { function ServerStatus(): ReactElement | null {
const serverSettings = useLoaderData() as ServerSettings const serverSettings = useLoaderData() as ServerSettings
const {t} = useTranslation() const {t} = useTranslation()
const {_decryptUsingPrivateKey} = useContext(AuthContext) const {_decryptUsingPrivateKey} = useContext(AuthContext)
const query = useQuery<CronReport, AxiosError>(["get_latest_cron_report"], async () => { const query = useQuery<CronReport | null, AxiosError>(["get_latest_cron_report"], async () => {
const encryptedReport = await getLatestCronReport() const {code, detail, ...encryptedReport} = await getLatestCronReport()
if (code === "error:cron_report:no_reports_found") {
return null
}
;(encryptedReport as any as CronReport).reportData = await decryptCronReportData( ;(encryptedReport as any as CronReport).reportData = await decryptCronReportData(
encryptedReport.reportData.encryptedReport, encryptedReport.reportData.encryptedReport,
@ -34,8 +39,12 @@ export default function ServerStatus(): ReactElement {
}) })
return ( return (
<QueryResult<CronReport> query={query}> <QueryResult<CronReport | null> query={query}>
{report => { {report => {
if (report === null) {
return null
}
const thresholdDate = subDays(new Date(), MAX_REPORT_DAY_THRESHOLD) const thresholdDate = subDays(new Date(), MAX_REPORT_DAY_THRESHOLD)
if (report.createdAt < thresholdDate) { if (report.createdAt < thresholdDate) {
@ -75,3 +84,5 @@ export default function ServerStatus(): ReactElement {
</QueryResult> </QueryResult>
) )
} }
export default WithEncryptionRequired(ServerStatus)

View File

@ -6,6 +6,9 @@ import {MdCheck, MdClear, MdOutlineChangeCircle, MdTextFormat} from "react-icons
import {BsImage} from "react-icons/bs" import {BsImage} from "react-icons/bs"
import {AxiosError} from "axios" import {AxiosError} from "axios"
import {FaMask} from "react-icons/fa"
import AliasesPercentageAmount from "./AliasPercentageAmount"
import { import {
Checkbox, Checkbox,
FormControlLabel, FormControlLabel,
@ -26,7 +29,6 @@ import {useErrorSuccessSnacks} from "~/hooks"
import {queryClient} from "~/constants/react-query" import {queryClient} from "~/constants/react-query"
import {parseFastAPIError} from "~/utils" import {parseFastAPIError} from "~/utils"
import {DEFAULT_ADMIN_SETTINGS} from "~/constants/admin-settings" import {DEFAULT_ADMIN_SETTINGS} from "~/constants/admin-settings"
import AliasesPercentageAmount from "./AliasPercentageAmount"
import RandomAliasGenerator from "~/route-widgets/GlobalSettingsRoute/RandomAliasGenerator" import RandomAliasGenerator from "~/route-widgets/GlobalSettingsRoute/RandomAliasGenerator"
export interface SettingsFormProps { export interface SettingsFormProps {
@ -83,40 +85,31 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
allowStatistics: yup allowStatistics: yup
.boolean() .boolean()
.label(t("routes.AdminRoute.forms.settings.allowStatistics.label")), .label(t("routes.AdminRoute.forms.settings.allowStatistics.label")),
}) allowAliasDeletion: yup
.boolean()
.label(t("routes.AdminRoute.forms.settings.allowAliasDeletion.label")),
maxAliasesPerUser: yup
.number()
.label(t("routes.AdminRoute.forms.settings.maxAliasesPerUser.label"))
.min(0),
} as Record<keyof AdminSettings, any>)
const {mutateAsync} = useMutation< const {mutateAsync} = useMutation<
UpdateAdminSettingsResponse, UpdateAdminSettingsResponse,
AxiosError, AxiosError,
Partial<AdminSettings> Partial<AdminSettings>
>( >(async settings => updateAdminSettings(settings), {
async settings => { onError: showError,
// Set values to `null` that are their defaults onSuccess: ({code, detail, ...newSettings}) => {
const strippedSettings = Object.fromEntries( if (code === "error:settings:global_settings_disabled") {
Object.entries(settings as AdminSettings).map(([key, value]) => { return
if (value === DEFAULT_ADMIN_SETTINGS[key as keyof AdminSettings]) { }
return [key, null]
}
return [key, value] showSuccess(t("routes.AdminRoute.settings.successMessage"))
}),
)
return updateAdminSettings(strippedSettings) queryClient.setQueryData<Partial<AdminSettings>>(queryKey, newSettings)
}, },
{ })
onError: showError,
onSuccess: ({code, detail, ...newSettings}) => {
if (code === "error:settings:global_settings_disabled") {
return
}
showSuccess(t("routes.AdminRoute.settings.successMessage"))
queryClient.setQueryData<Partial<AdminSettings>>(queryKey, newSettings)
},
},
)
const formik = useFormik<AdminSettings & {detail?: string}>({ const formik = useFormik<AdminSettings & {detail?: string}>({
validationSchema, validationSchema,
@ -159,7 +152,40 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
</Grid> </Grid>
<Grid item> <Grid item>
<Grid container spacing={3} direction="row" alignItems="start"> <Grid container spacing={3} direction="row" alignItems="start">
<Grid item md={6}> <Grid item xs={12}>
<TextField
key="max_aliases_per_user"
fullWidth
label={t(
"routes.AdminRoute.forms.settings.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(
"routes.AdminRoute.forms.settings.maxAliasesPerUser.description",
)
}
type="number"
disabled={formik.isSubmitting}
inputMode="numeric"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<FaMask />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<TextField <TextField
key="random_email_id_min_length" key="random_email_id_min_length"
fullWidth fullWidth
@ -192,7 +218,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
}} }}
/> />
</Grid> </Grid>
<Grid item md={6}> <Grid item xs={12} md={6}>
<StringPoolField <StringPoolField
fullWidth fullWidth
allowCustom allowCustom
@ -224,13 +250,13 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
} }
/> />
</Grid> </Grid>
<Grid item marginX="auto"> <Grid item xs={12} marginX="auto">
<RandomAliasGenerator <RandomAliasGenerator
characters={formik.values.randomEmailIdChars!} characters={formik.values.randomEmailIdChars!}
length={formik.values.randomEmailIdMinLength!} length={formik.values.randomEmailIdMinLength!}
/> />
</Grid> </Grid>
<Grid item> <Grid item xs={12}>
<TextField <TextField
key="random_email_length_increase_on_percentage" key="random_email_length_increase_on_percentage"
fullWidth fullWidth
@ -263,14 +289,14 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
}} }}
/> />
</Grid> </Grid>
<Grid item> <Grid item xs={12}>
<AliasesPercentageAmount <AliasesPercentageAmount
percentage={formik.values.randomEmailLengthIncreaseOnPercentage!} percentage={formik.values.randomEmailLengthIncreaseOnPercentage!}
length={formik.values.randomEmailIdMinLength!} length={formik.values.randomEmailIdMinLength!}
characters={formik.values.randomEmailIdChars!} characters={formik.values.randomEmailIdChars!}
/> />
</Grid> </Grid>
<Grid item md={6}> <Grid item xs={12} md={6}>
<TextField <TextField
key="custom_email_suffix_length" key="custom_email_suffix_length"
fullWidth fullWidth
@ -303,7 +329,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
}} }}
/> />
</Grid> </Grid>
<Grid item md={6}> <Grid item xs={12} md={6}>
<StringPoolField <StringPoolField
key="custom_email_suffix_chars" key="custom_email_suffix_chars"
allowCustom allowCustom
@ -335,7 +361,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
} }
/> />
</Grid> </Grid>
<Grid item> <Grid item xs={12}>
<TextField <TextField
key="image_proxy_storage_life_time_in_hours" key="image_proxy_storage_life_time_in_hours"
fullWidth fullWidth
@ -380,7 +406,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
}} }}
/> />
</Grid> </Grid>
<Grid item> <Grid item xs={12}>
<FormGroup key="user_email_enable_disposable_emails"> <FormGroup key="user_email_enable_disposable_emails">
<FormControlLabel <FormControlLabel
control={ control={
@ -409,12 +435,12 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
</FormHelperText> </FormHelperText>
</FormGroup> </FormGroup>
</Grid> </Grid>
<Grid item> <Grid item xs={12}>
<FormGroup key="user_email_enable_other_relays"> <FormGroup key="user_email_enable_other_relays">
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <Checkbox
checked={formik.values.userEmailEnableOtherRelays!} checked={formik.values.userEmailEnableOtherRelays}
onChange={formik.handleChange} onChange={formik.handleChange}
name="userEmailEnableOtherRelays" name="userEmailEnableOtherRelays"
/> />
@ -438,12 +464,12 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
</FormHelperText> </FormHelperText>
</FormGroup> </FormGroup>
</Grid> </Grid>
<Grid item> <Grid item xs={12}>
<FormGroup key="enable_image_proxy"> <FormGroup key="enable_image_proxy">
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <Checkbox
checked={formik.values.enableImageProxy!} checked={formik.values.enableImageProxy}
onChange={formik.handleChange} onChange={formik.handleChange}
name="enableImageProxy" name="enableImageProxy"
/> />
@ -467,12 +493,12 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
</FormHelperText> </FormHelperText>
</FormGroup> </FormGroup>
</Grid> </Grid>
<Grid item> <Grid item xs={12}>
<FormGroup key="allow_statistics"> <FormGroup key="allow_statistics">
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <Checkbox
checked={formik.values.allowStatistics!} checked={formik.values.allowStatistics}
onChange={formik.handleChange} onChange={formik.handleChange}
name="allowStatistics" name="allowStatistics"
/> />
@ -496,12 +522,12 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
</FormHelperText> </FormHelperText>
</FormGroup> </FormGroup>
</Grid> </Grid>
<Grid item> <Grid item xs={12}>
<FormGroup key="allow_alias_deletion"> <FormGroup key="allow_alias_deletion">
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <Checkbox
checked={formik.values.allowAliasDeletion!} checked={formik.values.allowAliasDeletion}
onChange={formik.handleChange} onChange={formik.handleChange}
name="allowAliasDeletion" name="allowAliasDeletion"
/> />

View File

@ -1,13 +1,11 @@
import {ReactElement} from "react" import {ReactElement} from "react"
import {AxiosError} from "axios" import {AxiosError} from "axios"
import _ from "lodash"
import {useQuery} from "@tanstack/react-query" import {useQuery} from "@tanstack/react-query"
import {AdminSettings} from "~/server-types" import {AdminSettings} from "~/server-types"
import {getAdminSettings} from "~/apis" import {getAdminSettings} from "~/apis"
import {QueryResult} from "~/components" import {QueryResult} from "~/components"
import {DEFAULT_ADMIN_SETTINGS} from "~/constants/admin-settings"
import SettingsDisabled from "~/route-widgets/GlobalSettingsRoute/SettingsDisabled" import SettingsDisabled from "~/route-widgets/GlobalSettingsRoute/SettingsDisabled"
import SettingsForm from "~/route-widgets/GlobalSettingsRoute/SettingsForm" import SettingsForm from "~/route-widgets/GlobalSettingsRoute/SettingsForm"
@ -19,9 +17,7 @@ export default function GlobalSettingsRoute(): ReactElement {
if (code === "error:settings:global_settings_disabled") { if (code === "error:settings:global_settings_disabled") {
return null return null
} else { } else {
return _.mergeWith({}, DEFAULT_ADMIN_SETTINGS, settings, (o, s) => return settings as AdminSettings
_.isNull(s) ? o : s,
) as AdminSettings
} }
}) })

View File

@ -201,17 +201,18 @@ export interface GetPageData {
} }
export interface AdminSettings { export interface AdminSettings {
randomEmailIdMinLength: number | null randomEmailIdMinLength: number
randomEmailIdChars: string | null randomEmailIdChars: string
randomEmailLengthIncreaseOnPercentage: number | null randomEmailLengthIncreaseOnPercentage: number
customEmailSuffixLength: number customEmailSuffixLength: number
customEmailSuffixChars: string | null customEmailSuffixChars: string
imageProxyStorageLifeTimeInHours: number imageProxyStorageLifeTimeInHours: number
enableImageProxy: boolean | null enableImageProxy: boolean
userEmailEnableDisposableEmails: boolean userEmailEnableDisposableEmails: boolean
userEmailEnableOtherRelays: boolean | null userEmailEnableOtherRelays: boolean
allowStatistics: boolean | null allowStatistics: boolean
allowAliasDeletion: boolean | null allowAliasDeletion: boolean
maxAliasesPerUser: number
} }
export interface ServerCronReport { export interface ServerCronReport {