diff --git a/index.html b/index.html index 31a68b8..e0d1c84 100755 --- a/index.html +++ b/index.html @@ -2,7 +2,6 @@ - Vite + React + TS diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index 0f99c43..ab1c5aa 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -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." } } }, diff --git a/src/apis/get-latest-cron-report.ts b/src/apis/get-latest-cron-report.ts index 17e8786..a705ef5 100644 --- a/src/apis/get-latest-cron-report.ts +++ b/src/apis/get-latest-cron-report.ts @@ -1,7 +1,12 @@ import {ServerCronReport} from "~/server-types" import {client} from "~/constants/axios-client" -export default async function getLatestCronReport(): Promise { +export type GetLatestCronReportResponse = ServerCronReport & { + detail: string + code: "error:cron_report:no_reports_found" +} + +export default async function getLatestCronReport(): Promise { const {data} = await client.get( `${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/cron-report/latest/`, { diff --git a/src/components/widgets/QueryResult.tsx b/src/components/widgets/QueryResult.tsx index 402b064..243ea2e 100644 --- a/src/components/widgets/QueryResult.tsx +++ b/src/components/widgets/QueryResult.tsx @@ -10,13 +10,14 @@ import LoadingData from "./LoadingData" export interface QueryResultProps { query: UseQueryResult - children: (data: TQueryFnData) => ReactElement + children: (data: TQueryFnData) => ReactElement | null } -export default function QueryResult({ - query, - children: render, -}: QueryResultProps): ReactElement { +export default function QueryResult< + TQueryFnData, + TError = AxiosError, + ReturnType = ReactElement | null, +>({query, children: render}: QueryResultProps): ReactElement | null { if (query.data !== undefined) { return render(query.data) } diff --git a/src/constants/admin-settings.ts b/src/constants/admin-settings.ts index b85bf6b..95d0edc 100644 --- a/src/constants/admin-settings.ts +++ b/src/constants/admin-settings.ts @@ -12,4 +12,5 @@ export const DEFAULT_ADMIN_SETTINGS: AdminSettings = { enableImageProxy: true, allowStatistics: true, allowAliasDeletion: false, + maxAliasesPerUser: 0, } diff --git a/src/route-widgets/AdminRoute/ServerStatus.tsx b/src/route-widgets/AdminRoute/ServerStatus.tsx index 8ed9c7f..8140b5e 100644 --- a/src/route-widgets/AdminRoute/ServerStatus.tsx +++ b/src/route-widgets/AdminRoute/ServerStatus.tsx @@ -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(["get_latest_cron_report"], async () => { - const encryptedReport = await getLatestCronReport() + const query = useQuery(["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 ( - query={query}> + 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 { ) } + +export default WithEncryptionRequired(ServerStatus) diff --git a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx index 87b542f..6737e73 100644 --- a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx +++ b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx @@ -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) const {mutateAsync} = useMutation< UpdateAdminSettingsResponse, AxiosError, Partial - >( - 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>(queryKey, newSettings) }, - { - onError: showError, - onSuccess: ({code, detail, ...newSettings}) => { - if (code === "error:settings:global_settings_disabled") { - return - } - - showSuccess(t("routes.AdminRoute.settings.successMessage")) - - queryClient.setQueryData>(queryKey, newSettings) - }, - }, - ) + }) const formik = useFormik({ validationSchema, @@ -159,7 +152,40 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { - + + + + + ), + }} + /> + + - + - + - + - + - + - + - + - + - + @@ -438,12 +464,12 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { - + @@ -467,12 +493,12 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { - + @@ -496,12 +522,12 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { - + diff --git a/src/routes/GlobalSettingsRoute.tsx b/src/routes/GlobalSettingsRoute.tsx index 59cfbbc..ef30997 100644 --- a/src/routes/GlobalSettingsRoute.tsx +++ b/src/routes/GlobalSettingsRoute.tsx @@ -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 } }) diff --git a/src/server-types.ts b/src/server-types.ts index 45192f4..bfe1b79 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -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 {