diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json index f67df95..0f99c43 100644 --- a/public/locales/en-US/translation.json +++ b/public/locales/en-US/translation.json @@ -16,7 +16,8 @@ "loading": "Loading...", "actionNotUndoable": "This action cannot be undone!", "copyError": "Copying to clipboard did not work. Please copy the text manually.", - "experimentalFeature": "This is an experimental feature." + "experimentalFeature": "This is an experimental feature.", + "deletedSuccessfully": "Deleted successfully!" }, "routes": { @@ -202,6 +203,13 @@ } } } + }, + "actions": { + "delete": { + "label": "Delete Alias", + "description": "Are you sure you want to delete this alias?", + "continueAction": "Delete Alias" + } } }, "ReportsRoute": { @@ -383,6 +391,10 @@ "allowStatistics": { "label": "Allow statistics", "description": "If enabled, your instance will collect anonymous statistics and share them. They will only be stored locally on this instance but made public." + }, + "allowAliasDeletion": { + "label": "Allow alias deletion", + "description": "If enabled, users will be able to delete their aliases." } } }, @@ -395,7 +407,25 @@ "helperText": "This is just a preview. Those are not real aliases." }, "randomAliasesIncreaseExplanation": "Random aliases' length will be increased from {{originalLength}} to {{increasedLength}} characters after {{amount}} aliases have been created.", - "resetLabel": "Reset to defaults" + "resetLabel": "Reset to defaults", + "disabled": { + "title": "Global settings are disabled", + "description": "Global settings have been disabled. You can enable them in the configuration file." + } + }, + "reservedAlias": { + "actions": { + "delete": { + "label": "Delete Reserved Alias", + "description": "Are you sure you want to delete this reserved alias?", + "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}}." } } }, @@ -501,7 +531,8 @@ "notesUpdated": "Updated & encrypted notes successfully!", "aliasChangedToEnabled": "Alias has been enabled", "aliasChangedToDisabled": "Alias has been disabled", - "addressCopiedToClipboard": "Address has been copied to your clipboard!" + "addressCopiedToClipboard": "Address has been copied to your clipboard!", + "aliasDeleted": "Alias has been deleted!" } }, "settings": { diff --git a/src/App.tsx b/src/App.tsx index 75f1d89..83d9842 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -81,7 +81,8 @@ const router = createBrowserRouter([ element: , }, { - path: "/aliases/:addressInBase64", + path: "/aliases/:id", + loader: getServerSettings, element: , }, { @@ -105,6 +106,7 @@ const router = createBrowserRouter([ }, { path: "/admin", + loader: getServerSettings, element: , }, { diff --git a/src/I18nHandler.tsx b/src/I18nHandler.tsx index 47058d2..8237ff3 100644 --- a/src/I18nHandler.tsx +++ b/src/I18nHandler.tsx @@ -2,7 +2,7 @@ import * as yup from "yup" import {useTranslation} from "react-i18next" import {useEffect} from "react" import {de} from "yup-locales" -import en from "yup/es/locale" +import en from "yup/lib/locale" const YUP_LOCALE_LANGUAGE_MAP: Record = { "en-US": en, diff --git a/src/apis/delete-alias.ts b/src/apis/delete-alias.ts new file mode 100644 index 0000000..9d24d17 --- /dev/null +++ b/src/apis/delete-alias.ts @@ -0,0 +1,10 @@ +import {client} from "~/constants/axios-client" +import {SimpleDetailResponse} from "~/server-types" + +export default async function deleteAlias(id: string): Promise { + const {data} = await client.delete(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/alias/${id}`, { + withCredentials: true, + }) + + return data +} diff --git a/src/apis/delete-reserved-alias.ts b/src/apis/delete-reserved-alias.ts new file mode 100644 index 0000000..f3b019b --- /dev/null +++ b/src/apis/delete-reserved-alias.ts @@ -0,0 +1,13 @@ +import {client} from "~/constants/axios-client" +import {SimpleDetailResponse} from "~/server-types" + +export default async function deleteReservedAlias(id: string): Promise { + const {data} = await client.delete( + `${import.meta.env.VITE_SERVER_BASE_URL}/v1/reserved-alias/${id}`, + { + withCredentials: true, + }, + ) + + return data +} diff --git a/src/apis/get-admin-settings.ts b/src/apis/get-admin-settings.ts index afa9427..8754f03 100644 --- a/src/apis/get-admin-settings.ts +++ b/src/apis/get-admin-settings.ts @@ -1,7 +1,13 @@ import {client} from "~/constants/axios-client" import {AdminSettings} from "~/server-types" -export default async function getAdminSettings(): Promise> { +export type GetAdminSettingsResponse = + | Partial & { + detail: string + code: "error:settings:global_settings_disabled" + } + +export default async function getAdminSettings(): Promise { const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/settings`, { withCredentials: true, }) diff --git a/src/apis/get-alias.ts b/src/apis/get-alias.ts index 775af6e..00fdee9 100644 --- a/src/apis/get-alias.ts +++ b/src/apis/get-alias.ts @@ -1,8 +1,8 @@ import {client} from "~/constants/axios-client" import {Alias} from "~/server-types" -export default async function getAlias(address: string): Promise { - const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/alias/${address}`, { +export default async function getAlias(aliasID: string): Promise { + const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/alias/${aliasID}`, { withCredentials: true, }) diff --git a/src/apis/get-latest-cron-report.ts b/src/apis/get-latest-cron-report.ts new file mode 100644 index 0000000..17e8786 --- /dev/null +++ b/src/apis/get-latest-cron-report.ts @@ -0,0 +1,13 @@ +import {ServerCronReport} from "~/server-types" +import {client} from "~/constants/axios-client" + +export default async function getLatestCronReport(): Promise { + const {data} = await client.get( + `${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/cron-report/latest/`, + { + withCredentials: true, + }, + ) + + return data +} diff --git a/src/apis/get-me.ts b/src/apis/get-me.ts index c211f00..bee5c36 100644 --- a/src/apis/get-me.ts +++ b/src/apis/get-me.ts @@ -1,7 +1,7 @@ -import {AuthenticationDetails} from "~/server-types" +import {ServerUser} from "~/server-types" import {client} from "~/constants/axios-client" -export default async function getMe(): Promise { +export default async function getMe(): Promise { const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/account/me`, { 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/apis/index.ts b/src/apis/index.ts index bfb40f1..fbb1e32 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -52,3 +52,9 @@ export * from "./get-admin-settings" export {default as getAdminSettings} from "./get-admin-settings" export * from "./update-admin-settings" export {default as updateAdminSettings} from "./update-admin-settings" +export * from "./delete-alias" +export {default as deleteAlias} from "./delete-alias" +export * from "./delete-reserved-alias" +export {default as deleteReservedAlias} from "./delete-reserved-alias" +export * from "./get-latest-cron-report" +export {default as getLatestCronReport} from "./get-latest-cron-report" diff --git a/src/apis/refresh-token.ts b/src/apis/refresh-token.ts index 8d05f55..55b5d09 100644 --- a/src/apis/refresh-token.ts +++ b/src/apis/refresh-token.ts @@ -1,14 +1,9 @@ import {ServerUser} from "~/server-types" import {client} from "~/constants/axios-client" -export interface RefreshTokenResult { - user: ServerUser - detail: string -} - export const REFRESH_TOKEN_URL = `${import.meta.env.VITE_SERVER_BASE_URL}/v1/auth/refresh` -export default async function refreshToken(): Promise { +export default async function refreshToken(): Promise { const {data} = await client.post( REFRESH_TOKEN_URL, {}, diff --git a/src/apis/resend-email-login-code.ts b/src/apis/resend-email-login-code.ts index 97381ff..28ff7f3 100644 --- a/src/apis/resend-email-login-code.ts +++ b/src/apis/resend-email-login-code.ts @@ -1,5 +1,5 @@ -import {SimpleDetailResponse} from "~/server-types" import {client} from "~/constants/axios-client" +import {SimpleDetailResponse} from "~/server-types" export interface ResendEmailLoginCodeData { email: string diff --git a/src/apis/resend-email-verification-code.ts b/src/apis/resend-email-verification-code.ts index 67f90ae..a46cc4d 100644 --- a/src/apis/resend-email-verification-code.ts +++ b/src/apis/resend-email-verification-code.ts @@ -1,9 +1,15 @@ import {SimpleDetailResponse} from "~/server-types" import {client} from "~/constants/axios-client" +export type ResendEmailVerificationCodeResponse = + | SimpleDetailResponse & { + detail: string + code: "ok:email_already_verified" + } + export default async function resendEmailVerificationCode( email: string, -): Promise { +): Promise { const {data} = await client.post( `${import.meta.env.VITE_SERVER_BASE_URL}/v1/auth/resend-email`, { diff --git a/src/apis/update-account.ts b/src/apis/update-account.ts index 5961783..efb5d2a 100644 --- a/src/apis/update-account.ts +++ b/src/apis/update-account.ts @@ -1,4 +1,4 @@ -import {AuthenticationDetails, Language} from "~/server-types" +import {Language, ServerUser} from "~/server-types" import {client} from "~/constants/axios-client" import parseUser from "~/apis/helpers/parse-user" @@ -9,10 +9,8 @@ export interface UpdateAccountData { language?: Language } -export default async function updateAccount( - updateData: UpdateAccountData, -): Promise { - const {data} = await client.patch( +export default async function updateAccount(updateData: UpdateAccountData): Promise { + const {data: user} = await client.patch( `${import.meta.env.VITE_SERVER_BASE_URL}/v1/account`, updateData, { @@ -20,8 +18,5 @@ export default async function updateAccount( }, ) - return { - ...data, - user: parseUser(data.user), - } + return parseUser(user) } diff --git a/src/apis/update-admin-settings.ts b/src/apis/update-admin-settings.ts index 5be9fd9..e92d99d 100644 --- a/src/apis/update-admin-settings.ts +++ b/src/apis/update-admin-settings.ts @@ -1,9 +1,15 @@ import {client} from "~/constants/axios-client" import {AdminSettings} from "~/server-types" +export type UpdateAdminSettingsResponse = + | Partial & { + detail: string + code: "error:settings:global_settings_disabled" + } + export default async function updateAdminSettings( settings: Partial, -): Promise { +): Promise { const {data} = await client.patch( `${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/settings`, settings, diff --git a/src/apis/verify-email.ts b/src/apis/verify-email.ts index 89ca3ad..65e84a0 100644 --- a/src/apis/verify-email.ts +++ b/src/apis/verify-email.ts @@ -1,4 +1,4 @@ -import {AuthenticationDetails} from "~/server-types" +import {ServerUser} from "~/server-types" import {client} from "~/constants/axios-client" import parseUser from "~/apis/helpers/parse-user" @@ -7,11 +7,8 @@ export interface VerifyEmailData { token: string } -export default async function verifyEmail({ - email, - token, -}: VerifyEmailData): Promise { - const {data} = await client.post( +export default async function verifyEmail({email, token}: VerifyEmailData): Promise { + const {data: user} = await client.post( `${import.meta.env.VITE_SERVER_BASE_URL}/v1/auth/verify-email`, { email: email, @@ -22,8 +19,5 @@ export default async function verifyEmail({ }, ) - return { - ...data, - user: parseUser(data.user), - } + return parseUser(user) } diff --git a/src/apis/verify-login-with-email.ts b/src/apis/verify-login-with-email.ts index 229204e..9ae6004 100644 --- a/src/apis/verify-login-with-email.ts +++ b/src/apis/verify-login-with-email.ts @@ -1,4 +1,4 @@ -import {AuthenticationDetails} from "~/server-types" +import {ServerUser} from "~/server-types" import {client} from "~/constants/axios-client" import parseUser from "~/apis/helpers/parse-user" @@ -12,8 +12,8 @@ export default async function verifyLoginWithEmail({ email, token, sameRequestToken, -}: VerifyLoginWithEmailData): Promise { - const {data} = await client.post( +}: VerifyLoginWithEmailData): Promise { + const {data: user} = await client.post( `${import.meta.env.VITE_SERVER_BASE_URL}/v1/auth/login/email-token/verify`, { email, @@ -25,8 +25,5 @@ export default async function verifyLoginWithEmail({ }, ) - return { - ...data, - user: parseUser(data.user), - } + return parseUser(user) } diff --git a/src/components/AuthContext/use-user.ts b/src/components/AuthContext/use-user.ts index b9e44c4..988cc82 100644 --- a/src/components/AuthContext/use-user.ts +++ b/src/components/AuthContext/use-user.ts @@ -3,8 +3,8 @@ import {AxiosError} from "axios" import {useMutation, useQuery} from "@tanstack/react-query" -import {REFRESH_TOKEN_URL, RefreshTokenResult, getMe, refreshToken} from "~/apis" -import {AuthenticationDetails, ServerUser, User} from "~/server-types" +import {REFRESH_TOKEN_URL, getMe, refreshToken} from "~/apis" +import {ServerUser, User} from "~/server-types" import {client} from "~/constants/axios-client" export interface UseAuthData { @@ -22,11 +22,11 @@ export default function useUser({ user, updateUser, }: UseAuthData) { - const {mutateAsync: refresh} = useMutation(refreshToken, { - onError: () => logout(), + const {mutateAsync: refresh} = useMutation(refreshToken, { + onError: logout, }) - useQuery(["get_me"], getMe, { + useQuery(["get_me"], getMe, { refetchOnWindowFocus: "always", refetchOnReconnect: "always", retry: 2, diff --git a/src/route-widgets/ReportDetailRoute/DeleteButton.tsx b/src/components/widgets/DeleteAPIButton.tsx similarity index 66% rename from src/route-widgets/ReportDetailRoute/DeleteButton.tsx rename to src/components/widgets/DeleteAPIButton.tsx index 0cf44a2..9ea0038 100644 --- a/src/route-widgets/ReportDetailRoute/DeleteButton.tsx +++ b/src/components/widgets/DeleteAPIButton.tsx @@ -15,24 +15,35 @@ import { } from "@mui/material" import {useMutation} from "@tanstack/react-query" -import {deleteReport} from "~/apis" import {useErrorSuccessSnacks} from "~/hooks" -import {SimpleDetailResponse} from "~/server-types" -export interface DeleteButtonProps { - id: string +export interface DeleteAPIButtonProps { + onDelete: () => Promise + label: string + continueLabel?: string + + description?: string + successMessage?: string + navigateTo?: string } -export default function ReportDetailRoute({id}: DeleteButtonProps): ReactElement { +export default function DeleteAPIButton({ + onDelete, + successMessage, + label, + continueLabel, + description, + navigateTo = "/aliases", +}: DeleteAPIButtonProps): ReactElement { const {t} = useTranslation() const {showError, showSuccess} = useErrorSuccessSnacks() const navigate = useNavigate() - const {mutate} = useMutation(() => deleteReport(id), { + const {mutate} = useMutation(onDelete, { onError: showError, onSuccess: () => { - showSuccess(t("relations.report.mutations.success.reportDeleted")) - navigate("/reports") + showSuccess(successMessage || t("general.deletedSuccessfully")) + navigate(navigateTo) }, }) @@ -47,14 +58,12 @@ export default function ReportDetailRoute({id}: DeleteButtonProps): ReactElement startIcon={} onClick={() => setShowDeleteDialog(true)} > - {t("routes.ReportDetailRoute.actions.delete.label")} + {label} setShowDeleteDialog(false)}> - {t("routes.ReportDetailRoute.actions.delete.label")} + {label} - - {t("routes.ReportDetailRoute.actions.delete.description")} - + {description && {description}} {t("general.actionNotUndoable")} @@ -69,7 +78,7 @@ export default function ReportDetailRoute({id}: DeleteButtonProps): ReactElement color="error" onClick={() => mutate()} > - {t("routes.ReportDetailRoute.actions.delete.continueAction")} + {continueLabel} diff --git a/src/components/widgets/QueryResult.tsx b/src/components/widgets/QueryResult.tsx index b58593f..402b064 100644 --- a/src/components/widgets/QueryResult.tsx +++ b/src/components/widgets/QueryResult.tsx @@ -17,7 +17,7 @@ export default function QueryResult({ query, children: render, }: QueryResultProps): ReactElement { - if (query.data) { + if (query.data !== undefined) { return render(query.data) } diff --git a/src/components/widgets/index.ts b/src/components/widgets/index.ts index d25c848..7ff371e 100644 --- a/src/components/widgets/index.ts +++ b/src/components/widgets/index.ts @@ -45,6 +45,8 @@ export {default as LoadingData} from "./LoadingData" export * from "./ExternalLinkIndication" export {default as ExternalLinkIndication} from "./ExternalLinkIndication" export {default as ExtensionSignalHandler} from "./ExtensionalSignalHandler" +export * from "./DeleteAPIButton" +export {default as DeleteButton} from "./DeleteAPIButton" export * from "./StringPoolField" export * as SimplePageBuilder from "./simple-page-builder" diff --git a/src/constants/admin-settings.ts b/src/constants/admin-settings.ts index 5d340a0..b85bf6b 100644 --- a/src/constants/admin-settings.ts +++ b/src/constants/admin-settings.ts @@ -11,4 +11,5 @@ export const DEFAULT_ADMIN_SETTINGS: AdminSettings = { imageProxyStorageLifeTimeInHours: 24, enableImageProxy: true, allowStatistics: true, + allowAliasDeletion: false, } diff --git a/src/hooks/use-error-success-snacks.ts b/src/hooks/use-error-success-snacks.ts index 9640ed4..009b244 100644 --- a/src/hooks/use-error-success-snacks.ts +++ b/src/hooks/use-error-success-snacks.ts @@ -28,17 +28,18 @@ export default function useErrorSuccessSnacks(): UseErrorSuccessSnacksResult { }) } const showError = (error: Error) => { - const parsedError = parseFastAPIError(error as AxiosError) + let message - if ("detail" in parsedError) { - $errorSnackbarKey.current = enqueueSnackbar( - parsedError.detail || t("general.defaultError"), - { - variant: "error", - autoHideDuration: ERROR_SNACKBAR_SHOW_DURATION, - }, - ) - } + try { + const parsedError = parseFastAPIError(error as AxiosError) + + message = parsedError.detail + } catch (e) {} + + $errorSnackbarKey.current = enqueueSnackbar(message || t("general.defaultError"), { + variant: "error", + autoHideDuration: ERROR_SNACKBAR_SHOW_DURATION, + }) } return { 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/route-widgets/AliasesRoute/AliasesListItem.tsx b/src/route-widgets/AliasesRoute/AliasesListItem.tsx index a88fcb4..6693421 100644 --- a/src/route-widgets/AliasesRoute/AliasesListItem.tsx +++ b/src/route-widgets/AliasesRoute/AliasesListItem.tsx @@ -29,7 +29,7 @@ export default function AliasesListItem({ // @ts-ignore component={isInCopyAddressMode ? undefined : RouterLink} key={alias.id} - to={isInCopyAddressMode ? undefined : `/aliases/${btoa(address)}`} + to={isInCopyAddressMode ? undefined : `/aliases/${alias.id}`} onClick={(event: any) => { if (isInCopyAddressMode) { event.preventDefault() diff --git a/src/route-widgets/CompleteAccountRoute/PasswordForm.tsx b/src/route-widgets/CompleteAccountRoute/PasswordForm.tsx index e8dcf11..6c90a8a 100644 --- a/src/route-widgets/CompleteAccountRoute/PasswordForm.tsx +++ b/src/route-widgets/CompleteAccountRoute/PasswordForm.tsx @@ -13,7 +13,7 @@ import {useMutation} from "@tanstack/react-query" import {AuthContext, PasswordField, SimpleForm} from "~/components" import {setupEncryptionForUser} from "~/utils" import {useExtensionHandler, useNavigateToNext, useSystemPreferredTheme, useUser} from "~/hooks" -import {AuthenticationDetails, ServerSettings} from "~/server-types" +import {ServerSettings, ServerUser} from "~/server-types" import {UpdateAccountData, updateAccount} from "~/apis" export interface PasswordFormProps { @@ -51,9 +51,7 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement const {_setEncryptionPassword, login} = useContext(AuthContext) - const {mutateAsync} = useMutation( - updateAccount, - ) + const {mutateAsync} = useMutation(updateAccount) const formik = useFormik
({ validationSchema: schema, initialValues: { @@ -83,7 +81,7 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement encryptedNotes, }, { - onSuccess: ({user: newUser}) => { + onSuccess: newUser => { login(newUser) _setEncryptionPassword(encryptionPassword) navigateToNext() diff --git a/src/route-widgets/GlobalSettingsRoute/SettingsDisabled.tsx b/src/route-widgets/GlobalSettingsRoute/SettingsDisabled.tsx new file mode 100644 index 0000000..79a15e3 --- /dev/null +++ b/src/route-widgets/GlobalSettingsRoute/SettingsDisabled.tsx @@ -0,0 +1,38 @@ +import {RiAlertFill} from "react-icons/ri" +import {ReactElement} from "react" +import {useTranslation} from "react-i18next" + +import {Container, Grid, Typography} from "@mui/material" + +export default function SettingsDisabled(): ReactElement { + console.log("asdas") + const {t} = useTranslation() + + return ( + + + + + {t("routes.AdminRoute.settings.disabled.title")} + + + + + + + + {t("routes.AdminRoute.settings.disabled.description")} + + + + + ) +} diff --git a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx index 5207c2f..87b542f 100644 --- a/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx +++ b/src/route-widgets/GlobalSettingsRoute/SettingsForm.tsx @@ -21,7 +21,7 @@ import {useMutation} from "@tanstack/react-query" import {AdminSettings} from "~/server-types" import {StringPoolField, createPool} from "~/components" -import {updateAdminSettings} from "~/apis" +import {UpdateAdminSettingsResponse, updateAdminSettings} from "~/apis" import {useErrorSuccessSnacks} from "~/hooks" import {queryClient} from "~/constants/react-query" import {parseFastAPIError} from "~/utils" @@ -85,7 +85,11 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { .label(t("routes.AdminRoute.forms.settings.allowStatistics.label")), }) - const {mutateAsync} = useMutation>( + const {mutateAsync} = useMutation< + UpdateAdminSettingsResponse, + AxiosError, + Partial + >( async settings => { // Set values to `null` that are their defaults const strippedSettings = Object.fromEntries( @@ -102,10 +106,14 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { }, { onError: showError, - onSuccess: newSettings => { + onSuccess: ({code, detail, ...newSettings}) => { + if (code === "error:settings:global_settings_disabled") { + return + } + showSuccess(t("routes.AdminRoute.settings.successMessage")) - queryClient.setQueryData(queryKey, newSettings) + queryClient.setQueryData>(queryKey, newSettings) }, }, ) @@ -488,6 +496,35 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) { + + + + } + disabled={formik.isSubmitting} + label={t( + "routes.AdminRoute.forms.settings.allowAliasDeletion.label", + )} + /> + + {(formik.touched.allowAliasDeletion && + formik.errors.allowAliasDeletion) || + t( + "routes.AdminRoute.forms.settings.allowAliasDeletion.description", + )} + + + diff --git a/src/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm.tsx b/src/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm.tsx index 35aec05..cc89a1e 100644 --- a/src/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm.tsx +++ b/src/route-widgets/LoginRoute/ConfirmCodeForm/ConfirmCodeForm.tsx @@ -24,12 +24,7 @@ import { } from "@mui/material" import {LoadingButton} from "@mui/lab" -import { - AuthenticationDetails, - ServerSettings, - ServerUser, - SimpleDetailResponse, -} from "~/server-types" +import {ServerSettings, ServerUser, SimpleDetailResponse} from "~/server-types" import {VerifyLoginWithEmailData, verifyLoginWithEmail} from "~/apis" import {MultiStepFormElement} from "~/components" import {parseFastAPIError} from "~/utils" @@ -84,10 +79,10 @@ export default function ConfirmCodeForm({ .label(t("routes.LoginRoute.forms.confirmCode.form.code.label")), }) - const {mutateAsync} = useMutation( + const {mutateAsync} = useMutation( verifyLoginWithEmail, { - onSuccess: ({user}) => onConfirm(user), + onSuccess: onConfirm, }, ) const formik = useFormik({ diff --git a/src/route-widgets/LoginRoute/ConfirmCodeForm/ResendMailButton.tsx b/src/route-widgets/LoginRoute/ConfirmCodeForm/ResendMailButton.tsx index 3d52a46..06e3c13 100644 --- a/src/route-widgets/LoginRoute/ConfirmCodeForm/ResendMailButton.tsx +++ b/src/route-widgets/LoginRoute/ConfirmCodeForm/ResendMailButton.tsx @@ -6,9 +6,9 @@ import React, {ReactElement} from "react" import {useMutation} from "@tanstack/react-query" -import {resendEmailLoginCode} from "~/apis" +import {ResendEmailLoginCodeResponse, resendEmailLoginCode} from "~/apis" import {MutationStatusSnackbar, TimedButton} from "~/components" -import {ServerSettings, SimpleDetailResponse} from "~/server-types" +import {ServerSettings} from "~/server-types" export interface ResendMailButtonProps { email: string @@ -22,7 +22,7 @@ export default function ResendMailButton({ const settings = useLoaderData() as ServerSettings const {t} = useTranslation() - const mutation = useMutation(() => + const mutation = useMutation(() => resendEmailLoginCode({ email, sameRequestToken, diff --git a/src/route-widgets/LoginRoute/ConfirmFromDifferentDevice.tsx b/src/route-widgets/LoginRoute/ConfirmFromDifferentDevice.tsx index 905171f..5f0181e 100644 --- a/src/route-widgets/LoginRoute/ConfirmFromDifferentDevice.tsx +++ b/src/route-widgets/LoginRoute/ConfirmFromDifferentDevice.tsx @@ -6,7 +6,7 @@ import {useTranslation} from "react-i18next" import {useMutation} from "@tanstack/react-query" import {Box, Grid, Paper, Typography} from "@mui/material" -import {AuthenticationDetails, ServerUser} from "~/server-types" +import {ServerUser} from "~/server-types" import {verifyLoginWithEmail} from "~/apis" import {LoadingData} from "~/components" @@ -23,14 +23,14 @@ export default function ConfirmFromDifferentDevice({ onConfirm, }: ConfirmFromDifferentDeviceProps): ReactElement { const {t} = useTranslation() - const {mutate, isLoading, isError} = useMutation( + const {mutate, isLoading, isError} = useMutation( () => verifyLoginWithEmail({ email, token, }), { - onSuccess: ({user}) => onConfirm(user), + onSuccess: onConfirm, }, ) diff --git a/src/route-widgets/SignupRoute/YouGotMail/ResendMailButton.tsx b/src/route-widgets/SignupRoute/YouGotMail/ResendMailButton.tsx index b049c9e..a6426dc 100644 --- a/src/route-widgets/SignupRoute/YouGotMail/ResendMailButton.tsx +++ b/src/route-widgets/SignupRoute/YouGotMail/ResendMailButton.tsx @@ -6,20 +6,31 @@ import React, {ReactElement} from "react" import {useMutation} from "@tanstack/react-query" -import {resendEmailVerificationCode} from "~/apis" +import {ResendEmailVerificationCodeResponse, resendEmailVerificationCode} from "~/apis" import {MutationStatusSnackbar, TimedButton} from "~/components" -import {ServerSettings, SimpleDetailResponse} from "~/server-types" +import {ServerSettings} from "~/server-types" export interface ResendMailButtonProps { email: string + onEmailAlreadyVerified: () => void } -export default function ResendMailButton({email}: ResendMailButtonProps): ReactElement { +export default function ResendMailButton({ + email, + onEmailAlreadyVerified, +}: ResendMailButtonProps): ReactElement { const {t} = useTranslation() const settings = useLoaderData() as ServerSettings - const mutation = useMutation(() => - resendEmailVerificationCode(email), + const mutation = useMutation( + () => resendEmailVerificationCode(email), + { + onSuccess: ({code}: any) => { + if (code === "ok:email_already_verified") { + onEmailAlreadyVerified() + } + }, + }, ) const {mutate} = mutation diff --git a/src/route-widgets/SignupRoute/YouGotMail/YouGotMail.tsx b/src/route-widgets/SignupRoute/YouGotMail/YouGotMail.tsx index 5fa50bd..c01de46 100644 --- a/src/route-widgets/SignupRoute/YouGotMail/YouGotMail.tsx +++ b/src/route-widgets/SignupRoute/YouGotMail/YouGotMail.tsx @@ -67,7 +67,7 @@ export default function YouGotMail({email, onGoBack}: YouGotMailProps): ReactEle - + 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/routes/AliasDetailRoute.tsx b/src/routes/AliasDetailRoute.tsx index d291426..ce85126 100644 --- a/src/routes/AliasDetailRoute.tsx +++ b/src/routes/AliasDetailRoute.tsx @@ -1,17 +1,18 @@ import {ReactElement, useContext} from "react" -import {useParams} from "react-router-dom" +import {useLoaderData, useParams} from "react-router-dom" import {AxiosError} from "axios" import {useTranslation} from "react-i18next" import {useQuery} from "@tanstack/react-query" import {Grid} from "@mui/material" -import {getAlias} from "~/apis" -import {Alias, DecryptedAlias} from "~/server-types" +import {deleteAlias, getAlias} from "~/apis" +import {Alias, DecryptedAlias, ServerSettings} from "~/server-types" import { AliasTypeIndicator, AuthContext, DecryptionPasswordMissingAlert, + DeleteButton, EncryptionStatus, QueryResult, SimplePage, @@ -25,13 +26,13 @@ import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes" export default function AliasDetailRoute(): ReactElement { const {t} = useTranslation() - const params = useParams() - const address = atob(params.addressInBase64 as string) + const serverSettings = useLoaderData() as ServerSettings + const {id: aliasID} = useParams() const {_decryptUsingMasterPassword, encryptionStatus} = useContext(AuthContext) - const queryKey = ["get_alias", address, encryptionStatus] + const queryKey = ["get_alias", aliasID, encryptionStatus] const query = useQuery(queryKey, async () => { - const alias = await getAlias(address) + const alias = await getAlias(aliasID!) if (encryptionStatus === EncryptionStatus.Available) { ;(alias as any as DecryptedAlias).notes = decryptAliasNotes( @@ -44,7 +45,22 @@ export default function AliasDetailRoute(): ReactElement { }) return ( - + deleteAlias(aliasID!)} + label={t("routes.AliasDetailRoute.actions.delete.label")} + description={t("routes.AliasDetailRoute.actions.delete.description")} + continueLabel={t("routes.AliasDetailRoute.actions.delete.continueAction")} + navigateTo={"/aliases"} + successMessage={t("relations.alias.mutations.success.aliasDeleted")} + /> + ) + } + > query={query}> {alias => ( @@ -60,7 +76,7 @@ export default function AliasDetailRoute(): ReactElement { - + (queryKey, async () => { - const settings = getAdminSettings() + const query = useQuery(queryKey, async () => { + const {code, detail, ...settings} = await getAdminSettings() - return _.mergeWith({}, DEFAULT_ADMIN_SETTINGS, settings, (o, s) => - _.isNull(s) ? o : s, - ) as AdminSettings + 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 ( - query={query}> - {settings => } + query={query}> + {settings => + settings === null ? ( + + ) : ( + + ) + } ) } diff --git a/src/routes/ReportDetailRoute.tsx b/src/routes/ReportDetailRoute.tsx index aec204d..7d4b36f 100644 --- a/src/routes/ReportDetailRoute.tsx +++ b/src/routes/ReportDetailRoute.tsx @@ -7,10 +7,15 @@ import {useQuery} from "@tanstack/react-query" import {List} from "@mui/material" import {DecryptedReportContent, Report} from "~/server-types" -import {getReport} from "~/apis" -import {DecryptReport, QueryResult, SimpleOverlayInformation, SimplePageBuilder} from "~/components" +import {deleteReport, getReport} from "~/apis" +import { + DecryptReport, + DeleteButton, + QueryResult, + SimpleOverlayInformation, + SimplePageBuilder, +} from "~/components" import {WithEncryptionRequired} from "~/hocs" -import DeleteButton from "~/route-widgets/ReportDetailRoute/DeleteButton" import ExpandedUrlsListItem from "~/route-widgets/ReportDetailRoute/ExpandedUrlsListItem" import ProxiedImagesListItem from "~/route-widgets/ReportDetailRoute/ProxiedImagesListItem" import SinglePixelImageTrackersListItem from "~/route-widgets/ReportDetailRoute/SinglePixelImageTrackersListItem" @@ -26,7 +31,18 @@ function ReportDetailRoute(): ReactElement { return ( } + actions={ + query.data && ( + deleteReport(params.id!)} + label={t("routes.ReportDetailRoute.actions.delete.label")} + description={t("routes.ReportDetailRoute.actions.delete.description")} + continueLabel={t("routes.ReportDetailRoute.actions.delete.continueAction")} + navigateTo={"/reports"} + successMessage={t("relations.report.mutations.success.reportDeleted")} + /> + ) + } > query={query}> {encryptedReport => ( diff --git a/src/routes/ReservedAliasDetailRoute.tsx b/src/routes/ReservedAliasDetailRoute.tsx index b3c9eac..74cbdbb 100644 --- a/src/routes/ReservedAliasDetailRoute.tsx +++ b/src/routes/ReservedAliasDetailRoute.tsx @@ -6,9 +6,9 @@ import {AxiosError} from "axios" import {useQuery} from "@tanstack/react-query" import {Grid} from "@mui/material" -import {QueryResult, SimplePage, SimplePageBuilder} from "~/components" +import {DeleteButton, QueryResult, SimplePage, SimplePageBuilder} from "~/components" import {ReservedAlias} from "~/server-types" -import {getReservedAlias} from "~/apis" +import {deleteReservedAlias, getReservedAlias} from "~/apis" import AliasActivationSwitch from "~/route-widgets/ReservedAliasDetailRoute/AliasActivationSwitch" import AliasAddress from "~/route-widgets/AliasDetailRoute/AliasAddress" import AliasUsersList from "~/route-widgets/ReservedAliasDetailRoute/AliasUsersList" @@ -21,7 +21,25 @@ export default function ReservedAliasDetailRoute(): ReactElement { const query = useQuery(queryKey, () => getReservedAlias(params.id!)) return ( - + deleteReservedAlias(params.id!)} + label={t("routes.AdminRoute.reservedAlias.actions.delete.label")} + description={t( + "routes.adminRoute.reservedAlias.actions.delete.description", + )} + continueLabel={t( + "routes.AdminRoute.reservedAlias.actions.delete.continueAction", + )} + navigateTo="/admin/reserved-aliases" + successMessage={t("relations.alias.mutations.success.aliasDeleted")} + /> + ) + } + > query={query}> {alias => ( diff --git a/src/routes/VerifyEmailRoute.tsx b/src/routes/VerifyEmailRoute.tsx index b5a8786..5641ecc 100644 --- a/src/routes/VerifyEmailRoute.tsx +++ b/src/routes/VerifyEmailRoute.tsx @@ -9,7 +9,7 @@ import React, {ReactElement, useContext} from "react" import {Grid, Paper, Typography, useTheme} from "@mui/material" import {useMutation} from "@tanstack/react-query" -import {AuthenticationDetails, ServerSettings} from "~/server-types" +import {ServerSettings, ServerUser} from "~/server-types" import {VerifyEmailData, verifyEmail} from "~/apis" import {useQueryParams} from "~/hooks" import {AuthContext} from "~/components" @@ -41,15 +41,12 @@ export default function VerifyEmailRoute(): ReactElement { return token.split("").every(char => chars.includes(char)) }) - const {mutateAsync} = useMutation( - verifyEmail, - { - onSuccess: ({user}) => { - login(user) - navigate("/auth/complete-account") - }, + const {mutateAsync} = useMutation(verifyEmail, { + onSuccess: user => { + login(user) + navigate("/auth/complete-account") }, - ) + }) const {loading} = useAsync(async () => { await emailSchema.validate(email) await tokenSchema.validate(token) diff --git a/src/server-types.ts b/src/server-types.ts index 6aa8310..45192f4 100644 --- a/src/server-types.ts +++ b/src/server-types.ts @@ -83,6 +83,7 @@ export interface ServerSettings { customAliasSuffixLength: number instanceSalt: string publicKey: string + allowAliasDeletion: boolean } export interface Alias { @@ -210,4 +211,30 @@ export interface AdminSettings { userEmailEnableDisposableEmails: boolean userEmailEnableOtherRelays: boolean | null allowStatistics: boolean | null + allowAliasDeletion: boolean | null +} + +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 + expiredReports: number + } + } }