mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-19 15:55:26 +02:00
Merge pull request #9 from Myzel394/update-admin-page
Update admin page
This commit is contained in:
commit
31f974368a
@ -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>
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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/`,
|
||||
{
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -12,4 +12,5 @@ export const DEFAULT_ADMIN_SETTINGS: AdminSettings = {
|
||||
enableImageProxy: true,
|
||||
allowStatistics: true,
|
||||
allowAliasDeletion: false,
|
||||
maxAliasesPerUser: 0,
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
/>
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user