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

View File

@ -378,7 +378,7 @@
},
"enableImageProxy": {
"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": {
"label": "Enable disposable emails for new accounts",
@ -395,6 +395,10 @@
"allowAliasDeletion": {
"label": "Allow alias deletion",
"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 {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(
`${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> {
query: UseQueryResult<TQueryFnData, TError>
children: (data: TQueryFnData) => ReactElement
children: (data: TQueryFnData) => ReactElement | null
}
export default function QueryResult<TQueryFnData, TError = AxiosError>({
query,
children: render,
}: QueryResultProps<TQueryFnData, TError>): ReactElement {
export default function QueryResult<
TQueryFnData,
TError = AxiosError,
ReturnType = ReactElement | null,
>({query, children: render}: QueryResultProps<TQueryFnData, TError>): ReactElement | null {
if (query.data !== undefined) {
return render(query.data)
}

View File

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

View File

@ -3,26 +3,31 @@ import {AxiosError} from "axios"
import {useLoaderData} from "react-router-dom"
import {useTranslation} from "react-i18next"
import format from "date-fns/format"
import formatRelative from "date-fns/formatRelative"
import subDays from "date-fns/subDays"
import {useQuery} from "@tanstack/react-query"
import {Alert} from "@mui/material"
import {WithEncryptionRequired} from "~/hocs"
import {CronReport, ServerSettings} from "~/server-types"
import {getLatestCronReport} from "~/apis"
import {AuthContext, QueryResult} from "~/components"
import decryptCronReportData from "~/apis/helpers/decrypt-cron-report-data"
import formatRelative from "date-fns/formatRelative"
const MAX_REPORT_DAY_THRESHOLD = 5
export default function ServerStatus(): ReactElement {
function ServerStatus(): ReactElement | null {
const serverSettings = useLoaderData() as ServerSettings
const {t} = useTranslation()
const {_decryptUsingPrivateKey} = useContext(AuthContext)
const query = useQuery<CronReport, AxiosError>(["get_latest_cron_report"], async () => {
const encryptedReport = await getLatestCronReport()
const query = useQuery<CronReport | null, AxiosError>(["get_latest_cron_report"], async () => {
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.reportData.encryptedReport,
@ -34,8 +39,12 @@ export default function ServerStatus(): ReactElement {
})
return (
<QueryResult<CronReport> query={query}>
<QueryResult<CronReport | null> query={query}>
{report => {
if (report === null) {
return null
}
const thresholdDate = subDays(new Date(), MAX_REPORT_DAY_THRESHOLD)
if (report.createdAt < thresholdDate) {
@ -75,3 +84,5 @@ export default function ServerStatus(): ReactElement {
</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 {AxiosError} from "axios"
import {FaMask} from "react-icons/fa"
import AliasesPercentageAmount from "./AliasPercentageAmount"
import {
Checkbox,
FormControlLabel,
@ -26,7 +29,6 @@ import {useErrorSuccessSnacks} from "~/hooks"
import {queryClient} from "~/constants/react-query"
import {parseFastAPIError} from "~/utils"
import {DEFAULT_ADMIN_SETTINGS} from "~/constants/admin-settings"
import AliasesPercentageAmount from "./AliasPercentageAmount"
import RandomAliasGenerator from "~/route-widgets/GlobalSettingsRoute/RandomAliasGenerator"
export interface SettingsFormProps {
@ -83,40 +85,31 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
allowStatistics: yup
.boolean()
.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<
UpdateAdminSettingsResponse,
AxiosError,
Partial<AdminSettings>
>(
async settings => {
// Set values to `null` that are their defaults
const strippedSettings = Object.fromEntries(
Object.entries(settings as AdminSettings).map(([key, value]) => {
if (value === DEFAULT_ADMIN_SETTINGS[key as keyof AdminSettings]) {
return [key, null]
}
>(async settings => updateAdminSettings(settings), {
onError: showError,
onSuccess: ({code, detail, ...newSettings}) => {
if (code === "error:settings:global_settings_disabled") {
return
}
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}>({
validationSchema,
@ -159,7 +152,40 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
</Grid>
<Grid item>
<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
key="random_email_id_min_length"
fullWidth
@ -192,7 +218,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
}}
/>
</Grid>
<Grid item md={6}>
<Grid item xs={12} md={6}>
<StringPoolField
fullWidth
allowCustom
@ -224,13 +250,13 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
}
/>
</Grid>
<Grid item marginX="auto">
<Grid item xs={12} marginX="auto">
<RandomAliasGenerator
characters={formik.values.randomEmailIdChars!}
length={formik.values.randomEmailIdMinLength!}
/>
</Grid>
<Grid item>
<Grid item xs={12}>
<TextField
key="random_email_length_increase_on_percentage"
fullWidth
@ -263,14 +289,14 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
}}
/>
</Grid>
<Grid item>
<Grid item xs={12}>
<AliasesPercentageAmount
percentage={formik.values.randomEmailLengthIncreaseOnPercentage!}
length={formik.values.randomEmailIdMinLength!}
characters={formik.values.randomEmailIdChars!}
/>
</Grid>
<Grid item md={6}>
<Grid item xs={12} md={6}>
<TextField
key="custom_email_suffix_length"
fullWidth
@ -303,7 +329,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
}}
/>
</Grid>
<Grid item md={6}>
<Grid item xs={12} md={6}>
<StringPoolField
key="custom_email_suffix_chars"
allowCustom
@ -335,7 +361,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
}
/>
</Grid>
<Grid item>
<Grid item xs={12}>
<TextField
key="image_proxy_storage_life_time_in_hours"
fullWidth
@ -380,7 +406,7 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
}}
/>
</Grid>
<Grid item>
<Grid item xs={12}>
<FormGroup key="user_email_enable_disposable_emails">
<FormControlLabel
control={
@ -409,12 +435,12 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
</FormHelperText>
</FormGroup>
</Grid>
<Grid item>
<Grid item xs={12}>
<FormGroup key="user_email_enable_other_relays">
<FormControlLabel
control={
<Checkbox
checked={formik.values.userEmailEnableOtherRelays!}
checked={formik.values.userEmailEnableOtherRelays}
onChange={formik.handleChange}
name="userEmailEnableOtherRelays"
/>
@ -438,12 +464,12 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
</FormHelperText>
</FormGroup>
</Grid>
<Grid item>
<Grid item xs={12}>
<FormGroup key="enable_image_proxy">
<FormControlLabel
control={
<Checkbox
checked={formik.values.enableImageProxy!}
checked={formik.values.enableImageProxy}
onChange={formik.handleChange}
name="enableImageProxy"
/>
@ -467,12 +493,12 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
</FormHelperText>
</FormGroup>
</Grid>
<Grid item>
<Grid item xs={12}>
<FormGroup key="allow_statistics">
<FormControlLabel
control={
<Checkbox
checked={formik.values.allowStatistics!}
checked={formik.values.allowStatistics}
onChange={formik.handleChange}
name="allowStatistics"
/>
@ -496,12 +522,12 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
</FormHelperText>
</FormGroup>
</Grid>
<Grid item>
<Grid item xs={12}>
<FormGroup key="allow_alias_deletion">
<FormControlLabel
control={
<Checkbox
checked={formik.values.allowAliasDeletion!}
checked={formik.values.allowAliasDeletion}
onChange={formik.handleChange}
name="allowAliasDeletion"
/>

View File

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

View File

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