diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json
index 6e65679..0f99c43 100644
--- a/public/locales/en-US/translation.json
+++ b/public/locales/en-US/translation.json
@@ -421,6 +421,11 @@
"continueAction": "Delete Reserved Alias"
}
}
+ },
+ "serverStatus": {
+ "noRecentReports": "There seems to be some issues with your server. The server hasn't done its cleanup in the last few days. The last report was on {{date}}.",
+ "error": "There was an error during the last server cleanup job from {{relativeDescription}}. Please check the logs for more information.",
+ "success": "Everything okay with your server! The last cleanup job was {{relativeDescription}}."
}
}
},
diff --git a/src/App.tsx b/src/App.tsx
index cf21964..83d9842 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -106,6 +106,7 @@ const router = createBrowserRouter([
},
{
path: "/admin",
+ loader: getServerSettings,
element: ,
},
{
diff --git a/src/apis/get-latest-cron-report.ts b/src/apis/get-latest-cron-report.ts
index b3ad51e..17e8786 100644
--- a/src/apis/get-latest-cron-report.ts
+++ b/src/apis/get-latest-cron-report.ts
@@ -1,9 +1,9 @@
-import {CronReport} from "~/server-types"
+import {ServerCronReport} from "~/server-types"
import {client} from "~/constants/axios-client"
-export default async function getLatestCronReport(): Promise {
+export default async function getLatestCronReport(): Promise {
const {data} = await client.get(
- `${import.meta.env.VITE_SERVER_BASE_URL}/v1/cron-report/latest/`,
+ `${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/cron-report/latest/`,
{
withCredentials: true,
},
diff --git a/src/apis/helpers/decrypt-cron-report-data.ts b/src/apis/helpers/decrypt-cron-report-data.ts
new file mode 100644
index 0000000..d03c0fc
--- /dev/null
+++ b/src/apis/helpers/decrypt-cron-report-data.ts
@@ -0,0 +1,33 @@
+import camelcaseKeys from "camelcase-keys"
+import update from "immutability-helper"
+
+import {AuthContextType} from "~/components"
+import {CronReport} from "~/server-types"
+import {extractCleartextFromSignedMessage} from "~/utils"
+
+export default async function decryptCronReportData(
+ signedMessage: string,
+ decryptContent: AuthContextType["_decryptUsingPrivateKey"],
+ publicKeyInPEM: string,
+): Promise {
+ const encryptedMessage = await extractCleartextFromSignedMessage(signedMessage, publicKeyInPEM)
+
+ return update(
+ camelcaseKeys(
+ JSON.parse(await decryptContent(encryptedMessage)) as CronReport["reportData"],
+ {
+ deep: true,
+ },
+ ),
+ {
+ report: {
+ startedAt: {
+ $apply: startedAt => new Date(startedAt),
+ },
+ finishedAt: {
+ $apply: finishedAt => new Date(finishedAt),
+ },
+ },
+ },
+ )
+}
diff --git a/src/route-widgets/AdminPage/ReservedAliasesList.tsx b/src/route-widgets/AdminPage/ReservedAliasesList.tsx
deleted file mode 100644
index b856845..0000000
--- a/src/route-widgets/AdminPage/ReservedAliasesList.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import {ReactElement} from "react"
-import {AxiosError} from "axios"
-
-import {useQuery} from "@tanstack/react-query"
-import {List, ListItem, ListItemText} from "@mui/material"
-
-import {getReservedAliases} from "~/apis"
-import {PaginationResult, ReservedAlias} from "~/server-types"
-import {QueryResult} from "~/components"
-
-export interface ReservedAliasesListProps {}
-
-export default function ReservedAliasesList({}: ReservedAliasesListProps): ReactElement {
- const query = useQuery, AxiosError>(
- ["getReservedAliases"],
- () => getReservedAliases(),
- )
-
- return (
- , AxiosError> query={query}>
- {({items}) => (
-
- {items.map(alias => (
-
-
-
- ))}
-
- )}
-
- )
-}
diff --git a/src/route-widgets/AdminRoute/ServerStatus.tsx b/src/route-widgets/AdminRoute/ServerStatus.tsx
new file mode 100644
index 0000000..8ed9c7f
--- /dev/null
+++ b/src/route-widgets/AdminRoute/ServerStatus.tsx
@@ -0,0 +1,77 @@
+import {ReactElement, useContext} from "react"
+import {AxiosError} from "axios"
+import {useLoaderData} from "react-router-dom"
+import {useTranslation} from "react-i18next"
+import format from "date-fns/format"
+import subDays from "date-fns/subDays"
+
+import {useQuery} from "@tanstack/react-query"
+import {Alert} from "@mui/material"
+
+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 {
+ const serverSettings = useLoaderData() as ServerSettings
+ const {t} = useTranslation()
+ const {_decryptUsingPrivateKey} = useContext(AuthContext)
+
+ const query = useQuery(["get_latest_cron_report"], async () => {
+ const encryptedReport = await getLatestCronReport()
+
+ ;(encryptedReport as any as CronReport).reportData = await decryptCronReportData(
+ encryptedReport.reportData.encryptedReport,
+ _decryptUsingPrivateKey,
+ serverSettings.publicKey,
+ )
+
+ return encryptedReport as any as CronReport
+ })
+
+ return (
+ query={query}>
+ {report => {
+ const thresholdDate = subDays(new Date(), MAX_REPORT_DAY_THRESHOLD)
+
+ if (report.createdAt < thresholdDate) {
+ return (
+
+ {t("routes.AdminRoute.serverStatus.noRecentReports", {
+ date: format(new Date(report.createdAt), "Pp"),
+ })}
+
+ )
+ }
+
+ if (report.reportData.report.status === "error") {
+ return (
+
+ {t("routes.AdminRoute.serverStatus.error", {
+ relativeDescription: formatRelative(
+ new Date(report.createdAt),
+ new Date(),
+ ),
+ })}
+
+ )
+ }
+
+ return (
+
+ {t("routes.AdminRoute.serverStatus.success", {
+ relativeDescription: formatRelative(
+ new Date(report.createdAt),
+ new Date(),
+ ),
+ })}
+
+ )
+ }}
+
+ )
+}
diff --git a/src/routes/AdminRoute.tsx b/src/routes/AdminRoute.tsx
index 37ce762..994dc15 100644
--- a/src/routes/AdminRoute.tsx
+++ b/src/routes/AdminRoute.tsx
@@ -8,14 +8,13 @@ import {List, ListItemButton, ListItemIcon, ListItemText} from "@mui/material"
import {SimplePageBuilder} from "~/components"
import {useNavigateToNext, useUser} from "~/hooks"
+import ServerStatus from "~/route-widgets/AdminRoute/ServerStatus"
export default function AdminRoute(): ReactElement {
const {t} = useTranslation()
const navigateToNext = useNavigateToNext()
const user = useUser()
- console.log(user)
-
useLayoutEffect(() => {
if (!user.isAdmin) {
navigateToNext()
@@ -24,6 +23,7 @@ export default function AdminRoute(): ReactElement {
return (
+
diff --git a/src/server-types.ts b/src/server-types.ts
index e8128fe..a51acbe 100644
--- a/src/server-types.ts
+++ b/src/server-types.ts
@@ -214,10 +214,26 @@ export interface AdminSettings {
allowAliasDeletion: boolean | null
}
-export interface CronReport {
+export interface ServerCronReport {
id: string
createdAt: Date
reportData: {
encryptedReport: string
}
}
+
+export interface CronReport {
+ id: string
+ createdAt: Date
+ reportData: {
+ version: "1.0"
+ id: string
+ report: {
+ startedAt: Date
+ finishedAt: Date
+ status: "success" | "error"
+ expiredImages: number
+ nonVerifiedUsers: number
+ }
+ }
+}