Merge pull request #2 from Myzel394/improvements

Improvements
This commit is contained in:
Myzel394 2023-02-18 12:15:44 +00:00 committed by GitHub
commit 63605bf5da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 500 additions and 171 deletions

View File

@ -16,7 +16,8 @@
"loading": "Loading...", "loading": "Loading...",
"actionNotUndoable": "This action cannot be undone!", "actionNotUndoable": "This action cannot be undone!",
"copyError": "Copying to clipboard did not work. Please copy the text manually.", "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": { "routes": {
@ -202,6 +203,13 @@
} }
} }
} }
},
"actions": {
"delete": {
"label": "Delete Alias",
"description": "Are you sure you want to delete this alias?",
"continueAction": "Delete Alias"
}
} }
}, },
"ReportsRoute": { "ReportsRoute": {
@ -383,6 +391,10 @@
"allowStatistics": { "allowStatistics": {
"label": "Allow statistics", "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." "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." "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.", "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!", "notesUpdated": "Updated & encrypted notes successfully!",
"aliasChangedToEnabled": "Alias has been enabled", "aliasChangedToEnabled": "Alias has been enabled",
"aliasChangedToDisabled": "Alias has been disabled", "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": { "settings": {

View File

@ -81,7 +81,8 @@ const router = createBrowserRouter([
element: <AliasesRoute />, element: <AliasesRoute />,
}, },
{ {
path: "/aliases/:addressInBase64", path: "/aliases/:id",
loader: getServerSettings,
element: <AliasDetailRoute />, element: <AliasDetailRoute />,
}, },
{ {
@ -105,6 +106,7 @@ const router = createBrowserRouter([
}, },
{ {
path: "/admin", path: "/admin",
loader: getServerSettings,
element: <AdminRoute />, element: <AdminRoute />,
}, },
{ {

View File

@ -2,7 +2,7 @@ import * as yup from "yup"
import {useTranslation} from "react-i18next" import {useTranslation} from "react-i18next"
import {useEffect} from "react" import {useEffect} from "react"
import {de} from "yup-locales" import {de} from "yup-locales"
import en from "yup/es/locale" import en from "yup/lib/locale"
const YUP_LOCALE_LANGUAGE_MAP: Record<string, unknown> = { const YUP_LOCALE_LANGUAGE_MAP: Record<string, unknown> = {
"en-US": en, "en-US": en,

10
src/apis/delete-alias.ts Normal file
View File

@ -0,0 +1,10 @@
import {client} from "~/constants/axios-client"
import {SimpleDetailResponse} from "~/server-types"
export default async function deleteAlias(id: string): Promise<SimpleDetailResponse> {
const {data} = await client.delete(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/alias/${id}`, {
withCredentials: true,
})
return data
}

View File

@ -0,0 +1,13 @@
import {client} from "~/constants/axios-client"
import {SimpleDetailResponse} from "~/server-types"
export default async function deleteReservedAlias(id: string): Promise<SimpleDetailResponse> {
const {data} = await client.delete(
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/reserved-alias/${id}`,
{
withCredentials: true,
},
)
return data
}

View File

@ -1,7 +1,13 @@
import {client} from "~/constants/axios-client" import {client} from "~/constants/axios-client"
import {AdminSettings} from "~/server-types" import {AdminSettings} from "~/server-types"
export default async function getAdminSettings(): Promise<Partial<AdminSettings>> { export type GetAdminSettingsResponse =
| Partial<AdminSettings> & {
detail: string
code: "error:settings:global_settings_disabled"
}
export default async function getAdminSettings(): Promise<GetAdminSettingsResponse> {
const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/settings`, { const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/settings`, {
withCredentials: true, withCredentials: true,
}) })

View File

@ -1,8 +1,8 @@
import {client} from "~/constants/axios-client" import {client} from "~/constants/axios-client"
import {Alias} from "~/server-types" import {Alias} from "~/server-types"
export default async function getAlias(address: string): Promise<Alias> { export default async function getAlias(aliasID: string): Promise<Alias> {
const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/alias/${address}`, { const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/alias/${aliasID}`, {
withCredentials: true, withCredentials: true,
}) })

View File

@ -0,0 +1,13 @@
import {ServerCronReport} from "~/server-types"
import {client} from "~/constants/axios-client"
export default async function getLatestCronReport(): Promise<ServerCronReport> {
const {data} = await client.get(
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/cron-report/latest/`,
{
withCredentials: true,
},
)
return data
}

View File

@ -1,7 +1,7 @@
import {AuthenticationDetails} from "~/server-types" import {ServerUser} from "~/server-types"
import {client} from "~/constants/axios-client" import {client} from "~/constants/axios-client"
export default async function getMe(): Promise<AuthenticationDetails> { export default async function getMe(): Promise<ServerUser> {
const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/account/me`, { const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/account/me`, {
withCredentials: true, withCredentials: true,
}) })

View File

@ -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<CronReport["reportData"]> {
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),
},
},
},
)
}

View File

@ -52,3 +52,9 @@ export * from "./get-admin-settings"
export {default as getAdminSettings} from "./get-admin-settings" export {default as getAdminSettings} from "./get-admin-settings"
export * from "./update-admin-settings" export * from "./update-admin-settings"
export {default as updateAdminSettings} 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"

View File

@ -1,14 +1,9 @@
import {ServerUser} from "~/server-types" import {ServerUser} from "~/server-types"
import {client} from "~/constants/axios-client" 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 const REFRESH_TOKEN_URL = `${import.meta.env.VITE_SERVER_BASE_URL}/v1/auth/refresh`
export default async function refreshToken(): Promise<RefreshTokenResult> { export default async function refreshToken(): Promise<ServerUser> {
const {data} = await client.post( const {data} = await client.post(
REFRESH_TOKEN_URL, REFRESH_TOKEN_URL,
{}, {},

View File

@ -1,5 +1,5 @@
import {SimpleDetailResponse} from "~/server-types"
import {client} from "~/constants/axios-client" import {client} from "~/constants/axios-client"
import {SimpleDetailResponse} from "~/server-types"
export interface ResendEmailLoginCodeData { export interface ResendEmailLoginCodeData {
email: string email: string

View File

@ -1,9 +1,15 @@
import {SimpleDetailResponse} from "~/server-types" import {SimpleDetailResponse} from "~/server-types"
import {client} from "~/constants/axios-client" import {client} from "~/constants/axios-client"
export type ResendEmailVerificationCodeResponse =
| SimpleDetailResponse & {
detail: string
code: "ok:email_already_verified"
}
export default async function resendEmailVerificationCode( export default async function resendEmailVerificationCode(
email: string, email: string,
): Promise<SimpleDetailResponse> { ): Promise<ResendEmailVerificationCodeResponse> {
const {data} = await client.post( const {data} = await client.post(
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/auth/resend-email`, `${import.meta.env.VITE_SERVER_BASE_URL}/v1/auth/resend-email`,
{ {

View File

@ -1,4 +1,4 @@
import {AuthenticationDetails, Language} from "~/server-types" import {Language, ServerUser} from "~/server-types"
import {client} from "~/constants/axios-client" import {client} from "~/constants/axios-client"
import parseUser from "~/apis/helpers/parse-user" import parseUser from "~/apis/helpers/parse-user"
@ -9,10 +9,8 @@ export interface UpdateAccountData {
language?: Language language?: Language
} }
export default async function updateAccount( export default async function updateAccount(updateData: UpdateAccountData): Promise<ServerUser> {
updateData: UpdateAccountData, const {data: user} = await client.patch(
): Promise<AuthenticationDetails> {
const {data} = await client.patch(
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/account`, `${import.meta.env.VITE_SERVER_BASE_URL}/v1/account`,
updateData, updateData,
{ {
@ -20,8 +18,5 @@ export default async function updateAccount(
}, },
) )
return { return parseUser(user)
...data,
user: parseUser(data.user),
}
} }

View File

@ -1,9 +1,15 @@
import {client} from "~/constants/axios-client" import {client} from "~/constants/axios-client"
import {AdminSettings} from "~/server-types" import {AdminSettings} from "~/server-types"
export type UpdateAdminSettingsResponse =
| Partial<AdminSettings> & {
detail: string
code: "error:settings:global_settings_disabled"
}
export default async function updateAdminSettings( export default async function updateAdminSettings(
settings: Partial<AdminSettings>, settings: Partial<AdminSettings>,
): Promise<AdminSettings> { ): Promise<UpdateAdminSettingsResponse> {
const {data} = await client.patch( const {data} = await client.patch(
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/settings`, `${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/settings`,
settings, settings,

View File

@ -1,4 +1,4 @@
import {AuthenticationDetails} from "~/server-types" import {ServerUser} from "~/server-types"
import {client} from "~/constants/axios-client" import {client} from "~/constants/axios-client"
import parseUser from "~/apis/helpers/parse-user" import parseUser from "~/apis/helpers/parse-user"
@ -7,11 +7,8 @@ export interface VerifyEmailData {
token: string token: string
} }
export default async function verifyEmail({ export default async function verifyEmail({email, token}: VerifyEmailData): Promise<ServerUser> {
email, const {data: user} = await client.post(
token,
}: VerifyEmailData): Promise<AuthenticationDetails> {
const {data} = await client.post(
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/auth/verify-email`, `${import.meta.env.VITE_SERVER_BASE_URL}/v1/auth/verify-email`,
{ {
email: email, email: email,
@ -22,8 +19,5 @@ export default async function verifyEmail({
}, },
) )
return { return parseUser(user)
...data,
user: parseUser(data.user),
}
} }

View File

@ -1,4 +1,4 @@
import {AuthenticationDetails} from "~/server-types" import {ServerUser} from "~/server-types"
import {client} from "~/constants/axios-client" import {client} from "~/constants/axios-client"
import parseUser from "~/apis/helpers/parse-user" import parseUser from "~/apis/helpers/parse-user"
@ -12,8 +12,8 @@ export default async function verifyLoginWithEmail({
email, email,
token, token,
sameRequestToken, sameRequestToken,
}: VerifyLoginWithEmailData): Promise<AuthenticationDetails> { }: VerifyLoginWithEmailData): Promise<ServerUser> {
const {data} = await client.post( const {data: user} = await client.post(
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/auth/login/email-token/verify`, `${import.meta.env.VITE_SERVER_BASE_URL}/v1/auth/login/email-token/verify`,
{ {
email, email,
@ -25,8 +25,5 @@ export default async function verifyLoginWithEmail({
}, },
) )
return { return parseUser(user)
...data,
user: parseUser(data.user),
}
} }

View File

@ -3,8 +3,8 @@ import {AxiosError} from "axios"
import {useMutation, useQuery} from "@tanstack/react-query" import {useMutation, useQuery} from "@tanstack/react-query"
import {REFRESH_TOKEN_URL, RefreshTokenResult, getMe, refreshToken} from "~/apis" import {REFRESH_TOKEN_URL, getMe, refreshToken} from "~/apis"
import {AuthenticationDetails, ServerUser, User} from "~/server-types" import {ServerUser, User} from "~/server-types"
import {client} from "~/constants/axios-client" import {client} from "~/constants/axios-client"
export interface UseAuthData { export interface UseAuthData {
@ -22,11 +22,11 @@ export default function useUser({
user, user,
updateUser, updateUser,
}: UseAuthData) { }: UseAuthData) {
const {mutateAsync: refresh} = useMutation<RefreshTokenResult, AxiosError, void>(refreshToken, { const {mutateAsync: refresh} = useMutation<ServerUser, AxiosError, void>(refreshToken, {
onError: () => logout(), onError: logout,
}) })
useQuery<AuthenticationDetails, AxiosError>(["get_me"], getMe, { useQuery<ServerUser, AxiosError>(["get_me"], getMe, {
refetchOnWindowFocus: "always", refetchOnWindowFocus: "always",
refetchOnReconnect: "always", refetchOnReconnect: "always",
retry: 2, retry: 2,

View File

@ -15,24 +15,35 @@ import {
} from "@mui/material" } from "@mui/material"
import {useMutation} from "@tanstack/react-query" import {useMutation} from "@tanstack/react-query"
import {deleteReport} from "~/apis"
import {useErrorSuccessSnacks} from "~/hooks" import {useErrorSuccessSnacks} from "~/hooks"
import {SimpleDetailResponse} from "~/server-types"
export interface DeleteButtonProps { export interface DeleteAPIButtonProps {
id: string onDelete: () => Promise<any>
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 {t} = useTranslation()
const {showError, showSuccess} = useErrorSuccessSnacks() const {showError, showSuccess} = useErrorSuccessSnacks()
const navigate = useNavigate() const navigate = useNavigate()
const {mutate} = useMutation<SimpleDetailResponse, AxiosError, void>(() => deleteReport(id), { const {mutate} = useMutation<void, AxiosError, void>(onDelete, {
onError: showError, onError: showError,
onSuccess: () => { onSuccess: () => {
showSuccess(t("relations.report.mutations.success.reportDeleted")) showSuccess(successMessage || t("general.deletedSuccessfully"))
navigate("/reports") navigate(navigateTo)
}, },
}) })
@ -47,14 +58,12 @@ export default function ReportDetailRoute({id}: DeleteButtonProps): ReactElement
startIcon={<MdDelete />} startIcon={<MdDelete />}
onClick={() => setShowDeleteDialog(true)} onClick={() => setShowDeleteDialog(true)}
> >
{t("routes.ReportDetailRoute.actions.delete.label")} {label}
</Button> </Button>
<Dialog open={showDeleteDialog} onClose={() => setShowDeleteDialog(false)}> <Dialog open={showDeleteDialog} onClose={() => setShowDeleteDialog(false)}>
<DialogTitle>{t("routes.ReportDetailRoute.actions.delete.label")}</DialogTitle> <DialogTitle>{label}</DialogTitle>
<DialogContent> <DialogContent>
<DialogContentText> {description && <DialogContentText>{description}</DialogContentText>}
{t("routes.ReportDetailRoute.actions.delete.description")}
</DialogContentText>
<DialogContentText color="error"> <DialogContentText color="error">
{t("general.actionNotUndoable")} {t("general.actionNotUndoable")}
</DialogContentText> </DialogContentText>
@ -69,7 +78,7 @@ export default function ReportDetailRoute({id}: DeleteButtonProps): ReactElement
color="error" color="error"
onClick={() => mutate()} onClick={() => mutate()}
> >
{t("routes.ReportDetailRoute.actions.delete.continueAction")} {continueLabel}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>

View File

@ -17,7 +17,7 @@ export default function QueryResult<TQueryFnData, TError = AxiosError>({
query, query,
children: render, children: render,
}: QueryResultProps<TQueryFnData, TError>): ReactElement { }: QueryResultProps<TQueryFnData, TError>): ReactElement {
if (query.data) { if (query.data !== undefined) {
return render(query.data) return render(query.data)
} }

View File

@ -45,6 +45,8 @@ export {default as LoadingData} from "./LoadingData"
export * from "./ExternalLinkIndication" export * from "./ExternalLinkIndication"
export {default as ExternalLinkIndication} from "./ExternalLinkIndication" export {default as ExternalLinkIndication} from "./ExternalLinkIndication"
export {default as ExtensionSignalHandler} from "./ExtensionalSignalHandler" export {default as ExtensionSignalHandler} from "./ExtensionalSignalHandler"
export * from "./DeleteAPIButton"
export {default as DeleteButton} from "./DeleteAPIButton"
export * from "./StringPoolField" export * from "./StringPoolField"
export * as SimplePageBuilder from "./simple-page-builder" export * as SimplePageBuilder from "./simple-page-builder"

View File

@ -11,4 +11,5 @@ export const DEFAULT_ADMIN_SETTINGS: AdminSettings = {
imageProxyStorageLifeTimeInHours: 24, imageProxyStorageLifeTimeInHours: 24,
enableImageProxy: true, enableImageProxy: true,
allowStatistics: true, allowStatistics: true,
allowAliasDeletion: false,
} }

View File

@ -28,17 +28,18 @@ export default function useErrorSuccessSnacks(): UseErrorSuccessSnacksResult {
}) })
} }
const showError = (error: Error) => { const showError = (error: Error) => {
let message
try {
const parsedError = parseFastAPIError(error as AxiosError) const parsedError = parseFastAPIError(error as AxiosError)
if ("detail" in parsedError) { message = parsedError.detail
$errorSnackbarKey.current = enqueueSnackbar( } catch (e) {}
parsedError.detail || t("general.defaultError"),
{ $errorSnackbarKey.current = enqueueSnackbar(message || t("general.defaultError"), {
variant: "error", variant: "error",
autoHideDuration: ERROR_SNACKBAR_SHOW_DURATION, autoHideDuration: ERROR_SNACKBAR_SHOW_DURATION,
}, })
)
}
} }
return { return {

View File

@ -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<PaginationResult<ReservedAlias>, AxiosError>(
["getReservedAliases"],
() => getReservedAliases(),
)
return (
<QueryResult<PaginationResult<ReservedAlias>, AxiosError> query={query}>
{({items}) => (
<List>
{items.map(alias => (
<ListItem key={alias.id}>
<ListItemText primary={`${alias.local}@${alias.domain}`} />
</ListItem>
))}
</List>
)}
</QueryResult>
)
}

View File

@ -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<CronReport, AxiosError>(["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 (
<QueryResult<CronReport> query={query}>
{report => {
const thresholdDate = subDays(new Date(), MAX_REPORT_DAY_THRESHOLD)
if (report.createdAt < thresholdDate) {
return (
<Alert severity="warning">
{t("routes.AdminRoute.serverStatus.noRecentReports", {
date: format(new Date(report.createdAt), "Pp"),
})}
</Alert>
)
}
if (report.reportData.report.status === "error") {
return (
<Alert severity="error">
{t("routes.AdminRoute.serverStatus.error", {
relativeDescription: formatRelative(
new Date(report.createdAt),
new Date(),
),
})}
</Alert>
)
}
return (
<Alert severity="success">
{t("routes.AdminRoute.serverStatus.success", {
relativeDescription: formatRelative(
new Date(report.createdAt),
new Date(),
),
})}
</Alert>
)
}}
</QueryResult>
)
}

View File

@ -29,7 +29,7 @@ export default function AliasesListItem({
// @ts-ignore // @ts-ignore
component={isInCopyAddressMode ? undefined : RouterLink} component={isInCopyAddressMode ? undefined : RouterLink}
key={alias.id} key={alias.id}
to={isInCopyAddressMode ? undefined : `/aliases/${btoa(address)}`} to={isInCopyAddressMode ? undefined : `/aliases/${alias.id}`}
onClick={(event: any) => { onClick={(event: any) => {
if (isInCopyAddressMode) { if (isInCopyAddressMode) {
event.preventDefault() event.preventDefault()

View File

@ -13,7 +13,7 @@ import {useMutation} from "@tanstack/react-query"
import {AuthContext, PasswordField, SimpleForm} from "~/components" import {AuthContext, PasswordField, SimpleForm} from "~/components"
import {setupEncryptionForUser} from "~/utils" import {setupEncryptionForUser} from "~/utils"
import {useExtensionHandler, useNavigateToNext, useSystemPreferredTheme, useUser} from "~/hooks" import {useExtensionHandler, useNavigateToNext, useSystemPreferredTheme, useUser} from "~/hooks"
import {AuthenticationDetails, ServerSettings} from "~/server-types" import {ServerSettings, ServerUser} from "~/server-types"
import {UpdateAccountData, updateAccount} from "~/apis" import {UpdateAccountData, updateAccount} from "~/apis"
export interface PasswordFormProps { export interface PasswordFormProps {
@ -51,9 +51,7 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
const {_setEncryptionPassword, login} = useContext(AuthContext) const {_setEncryptionPassword, login} = useContext(AuthContext)
const {mutateAsync} = useMutation<AuthenticationDetails, AxiosError, UpdateAccountData>( const {mutateAsync} = useMutation<ServerUser, AxiosError, UpdateAccountData>(updateAccount)
updateAccount,
)
const formik = useFormik<Form>({ const formik = useFormik<Form>({
validationSchema: schema, validationSchema: schema,
initialValues: { initialValues: {
@ -83,7 +81,7 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
encryptedNotes, encryptedNotes,
}, },
{ {
onSuccess: ({user: newUser}) => { onSuccess: newUser => {
login(newUser) login(newUser)
_setEncryptionPassword(encryptionPassword) _setEncryptionPassword(encryptionPassword)
navigateToNext() navigateToNext()

View File

@ -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 (
<Container maxWidth="xs">
<Grid
container
spacing={4}
direction="column"
alignItems="center"
maxWidth="80%"
alignSelf="center"
marginX="auto"
>
<Grid item>
<Typography variant="h6" component="h2">
{t("routes.AdminRoute.settings.disabled.title")}
</Typography>
</Grid>
<Grid item>
<RiAlertFill size={40} />
</Grid>
<Grid item>
<Typography variant="body1">
{t("routes.AdminRoute.settings.disabled.description")}
</Typography>
</Grid>
</Grid>
</Container>
)
}

View File

@ -21,7 +21,7 @@ import {useMutation} from "@tanstack/react-query"
import {AdminSettings} from "~/server-types" import {AdminSettings} from "~/server-types"
import {StringPoolField, createPool} from "~/components" import {StringPoolField, createPool} from "~/components"
import {updateAdminSettings} from "~/apis" import {UpdateAdminSettingsResponse, updateAdminSettings} from "~/apis"
import {useErrorSuccessSnacks} from "~/hooks" import {useErrorSuccessSnacks} from "~/hooks"
import {queryClient} from "~/constants/react-query" import {queryClient} from "~/constants/react-query"
import {parseFastAPIError} from "~/utils" import {parseFastAPIError} from "~/utils"
@ -85,7 +85,11 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
.label(t("routes.AdminRoute.forms.settings.allowStatistics.label")), .label(t("routes.AdminRoute.forms.settings.allowStatistics.label")),
}) })
const {mutateAsync} = useMutation<AdminSettings, AxiosError, Partial<AdminSettings>>( const {mutateAsync} = useMutation<
UpdateAdminSettingsResponse,
AxiosError,
Partial<AdminSettings>
>(
async settings => { async settings => {
// Set values to `null` that are their defaults // Set values to `null` that are their defaults
const strippedSettings = Object.fromEntries( const strippedSettings = Object.fromEntries(
@ -102,10 +106,14 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
}, },
{ {
onError: showError, onError: showError,
onSuccess: newSettings => { onSuccess: ({code, detail, ...newSettings}) => {
if (code === "error:settings:global_settings_disabled") {
return
}
showSuccess(t("routes.AdminRoute.settings.successMessage")) showSuccess(t("routes.AdminRoute.settings.successMessage"))
queryClient.setQueryData<AdminSettings>(queryKey, newSettings) queryClient.setQueryData<Partial<AdminSettings>>(queryKey, newSettings)
}, },
}, },
) )
@ -488,6 +496,35 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
</FormHelperText> </FormHelperText>
</FormGroup> </FormGroup>
</Grid> </Grid>
<Grid item>
<FormGroup key="allow_alias_deletion">
<FormControlLabel
control={
<Checkbox
checked={formik.values.allowAliasDeletion!}
onChange={formik.handleChange}
name="allowAliasDeletion"
/>
}
disabled={formik.isSubmitting}
label={t(
"routes.AdminRoute.forms.settings.allowAliasDeletion.label",
)}
/>
<FormHelperText
error={
formik.touched.allowAliasDeletion &&
Boolean(formik.errors.allowAliasDeletion)
}
>
{(formik.touched.allowAliasDeletion &&
formik.errors.allowAliasDeletion) ||
t(
"routes.AdminRoute.forms.settings.allowAliasDeletion.description",
)}
</FormHelperText>
</FormGroup>
</Grid>
</Grid> </Grid>
</Grid> </Grid>
<Grid item> <Grid item>

View File

@ -24,12 +24,7 @@ import {
} from "@mui/material" } from "@mui/material"
import {LoadingButton} from "@mui/lab" import {LoadingButton} from "@mui/lab"
import { import {ServerSettings, ServerUser, SimpleDetailResponse} from "~/server-types"
AuthenticationDetails,
ServerSettings,
ServerUser,
SimpleDetailResponse,
} from "~/server-types"
import {VerifyLoginWithEmailData, verifyLoginWithEmail} from "~/apis" import {VerifyLoginWithEmailData, verifyLoginWithEmail} from "~/apis"
import {MultiStepFormElement} from "~/components" import {MultiStepFormElement} from "~/components"
import {parseFastAPIError} from "~/utils" import {parseFastAPIError} from "~/utils"
@ -84,10 +79,10 @@ export default function ConfirmCodeForm({
.label(t("routes.LoginRoute.forms.confirmCode.form.code.label")), .label(t("routes.LoginRoute.forms.confirmCode.form.code.label")),
}) })
const {mutateAsync} = useMutation<AuthenticationDetails, AxiosError, VerifyLoginWithEmailData>( const {mutateAsync} = useMutation<ServerUser, AxiosError, VerifyLoginWithEmailData>(
verifyLoginWithEmail, verifyLoginWithEmail,
{ {
onSuccess: ({user}) => onConfirm(user), onSuccess: onConfirm,
}, },
) )
const formik = useFormik<Form>({ const formik = useFormik<Form>({

View File

@ -6,9 +6,9 @@ import React, {ReactElement} from "react"
import {useMutation} from "@tanstack/react-query" import {useMutation} from "@tanstack/react-query"
import {resendEmailLoginCode} from "~/apis" import {ResendEmailLoginCodeResponse, resendEmailLoginCode} from "~/apis"
import {MutationStatusSnackbar, TimedButton} from "~/components" import {MutationStatusSnackbar, TimedButton} from "~/components"
import {ServerSettings, SimpleDetailResponse} from "~/server-types" import {ServerSettings} from "~/server-types"
export interface ResendMailButtonProps { export interface ResendMailButtonProps {
email: string email: string
@ -22,7 +22,7 @@ export default function ResendMailButton({
const settings = useLoaderData() as ServerSettings const settings = useLoaderData() as ServerSettings
const {t} = useTranslation() const {t} = useTranslation()
const mutation = useMutation<SimpleDetailResponse, AxiosError, void>(() => const mutation = useMutation<ResendEmailLoginCodeResponse, AxiosError, void>(() =>
resendEmailLoginCode({ resendEmailLoginCode({
email, email,
sameRequestToken, sameRequestToken,

View File

@ -6,7 +6,7 @@ import {useTranslation} from "react-i18next"
import {useMutation} from "@tanstack/react-query" import {useMutation} from "@tanstack/react-query"
import {Box, Grid, Paper, Typography} from "@mui/material" import {Box, Grid, Paper, Typography} from "@mui/material"
import {AuthenticationDetails, ServerUser} from "~/server-types" import {ServerUser} from "~/server-types"
import {verifyLoginWithEmail} from "~/apis" import {verifyLoginWithEmail} from "~/apis"
import {LoadingData} from "~/components" import {LoadingData} from "~/components"
@ -23,14 +23,14 @@ export default function ConfirmFromDifferentDevice({
onConfirm, onConfirm,
}: ConfirmFromDifferentDeviceProps): ReactElement { }: ConfirmFromDifferentDeviceProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation()
const {mutate, isLoading, isError} = useMutation<AuthenticationDetails, AxiosError, void>( const {mutate, isLoading, isError} = useMutation<ServerUser, AxiosError, void>(
() => () =>
verifyLoginWithEmail({ verifyLoginWithEmail({
email, email,
token, token,
}), }),
{ {
onSuccess: ({user}) => onConfirm(user), onSuccess: onConfirm,
}, },
) )

View File

@ -6,20 +6,31 @@ import React, {ReactElement} from "react"
import {useMutation} from "@tanstack/react-query" import {useMutation} from "@tanstack/react-query"
import {resendEmailVerificationCode} from "~/apis" import {ResendEmailVerificationCodeResponse, resendEmailVerificationCode} from "~/apis"
import {MutationStatusSnackbar, TimedButton} from "~/components" import {MutationStatusSnackbar, TimedButton} from "~/components"
import {ServerSettings, SimpleDetailResponse} from "~/server-types" import {ServerSettings} from "~/server-types"
export interface ResendMailButtonProps { export interface ResendMailButtonProps {
email: string email: string
onEmailAlreadyVerified: () => void
} }
export default function ResendMailButton({email}: ResendMailButtonProps): ReactElement { export default function ResendMailButton({
email,
onEmailAlreadyVerified,
}: ResendMailButtonProps): ReactElement {
const {t} = useTranslation() const {t} = useTranslation()
const settings = useLoaderData() as ServerSettings const settings = useLoaderData() as ServerSettings
const mutation = useMutation<SimpleDetailResponse, AxiosError, void>(() => const mutation = useMutation<ResendEmailVerificationCodeResponse, AxiosError, void>(
resendEmailVerificationCode(email), () => resendEmailVerificationCode(email),
{
onSuccess: ({code}: any) => {
if (code === "ok:email_already_verified") {
onEmailAlreadyVerified()
}
},
},
) )
const {mutate} = mutation const {mutate} = mutation

View File

@ -67,7 +67,7 @@ export default function YouGotMail({email, onGoBack}: YouGotMailProps): ReactEle
<OpenMailButton domain={domain} /> <OpenMailButton domain={domain} />
</Grid> </Grid>
<Grid item> <Grid item>
<ResendMailButton email={email} /> <ResendMailButton email={email} onEmailAlreadyVerified={onGoBack} />
</Grid> </Grid>
</Grid> </Grid>
</MultiStepFormElement> </MultiStepFormElement>

View File

@ -8,14 +8,13 @@ import {List, ListItemButton, ListItemIcon, ListItemText} from "@mui/material"
import {SimplePageBuilder} from "~/components" import {SimplePageBuilder} from "~/components"
import {useNavigateToNext, useUser} from "~/hooks" import {useNavigateToNext, useUser} from "~/hooks"
import ServerStatus from "~/route-widgets/AdminRoute/ServerStatus"
export default function AdminRoute(): ReactElement { export default function AdminRoute(): ReactElement {
const {t} = useTranslation() const {t} = useTranslation()
const navigateToNext = useNavigateToNext() const navigateToNext = useNavigateToNext()
const user = useUser() const user = useUser()
console.log(user)
useLayoutEffect(() => { useLayoutEffect(() => {
if (!user.isAdmin) { if (!user.isAdmin) {
navigateToNext() navigateToNext()
@ -24,6 +23,7 @@ export default function AdminRoute(): ReactElement {
return ( return (
<SimplePageBuilder.Page title={t("routes.AdminRoute.title")}> <SimplePageBuilder.Page title={t("routes.AdminRoute.title")}>
<ServerStatus />
<List> <List>
<ListItemButton component={Link} to="/admin/reserved-aliases"> <ListItemButton component={Link} to="/admin/reserved-aliases">
<ListItemIcon> <ListItemIcon>

View File

@ -1,17 +1,18 @@
import {ReactElement, useContext} from "react" import {ReactElement, useContext} from "react"
import {useParams} from "react-router-dom" import {useLoaderData, useParams} from "react-router-dom"
import {AxiosError} from "axios" import {AxiosError} from "axios"
import {useTranslation} from "react-i18next" import {useTranslation} from "react-i18next"
import {useQuery} from "@tanstack/react-query" import {useQuery} from "@tanstack/react-query"
import {Grid} from "@mui/material" import {Grid} from "@mui/material"
import {getAlias} from "~/apis" import {deleteAlias, getAlias} from "~/apis"
import {Alias, DecryptedAlias} from "~/server-types" import {Alias, DecryptedAlias, ServerSettings} from "~/server-types"
import { import {
AliasTypeIndicator, AliasTypeIndicator,
AuthContext, AuthContext,
DecryptionPasswordMissingAlert, DecryptionPasswordMissingAlert,
DeleteButton,
EncryptionStatus, EncryptionStatus,
QueryResult, QueryResult,
SimplePage, SimplePage,
@ -25,13 +26,13 @@ import decryptAliasNotes from "~/apis/helpers/decrypt-alias-notes"
export default function AliasDetailRoute(): ReactElement { export default function AliasDetailRoute(): ReactElement {
const {t} = useTranslation() const {t} = useTranslation()
const params = useParams() const serverSettings = useLoaderData() as ServerSettings
const address = atob(params.addressInBase64 as string) const {id: aliasID} = useParams()
const {_decryptUsingMasterPassword, encryptionStatus} = useContext(AuthContext) const {_decryptUsingMasterPassword, encryptionStatus} = useContext(AuthContext)
const queryKey = ["get_alias", address, encryptionStatus] const queryKey = ["get_alias", aliasID, encryptionStatus]
const query = useQuery<Alias | DecryptedAlias, AxiosError>(queryKey, async () => { const query = useQuery<Alias | DecryptedAlias, AxiosError>(queryKey, async () => {
const alias = await getAlias(address) const alias = await getAlias(aliasID!)
if (encryptionStatus === EncryptionStatus.Available) { if (encryptionStatus === EncryptionStatus.Available) {
;(alias as any as DecryptedAlias).notes = decryptAliasNotes( ;(alias as any as DecryptedAlias).notes = decryptAliasNotes(
@ -44,7 +45,22 @@ export default function AliasDetailRoute(): ReactElement {
}) })
return ( return (
<SimplePage title={t("routes.AliasDetailRoute.title")}> <SimplePage
title={t("routes.AliasDetailRoute.title")}
actions={
serverSettings.allowAliasDeletion &&
query.data && (
<DeleteButton
onDelete={() => 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")}
/>
)
}
>
<QueryResult<Alias | DecryptedAlias> query={query}> <QueryResult<Alias | DecryptedAlias> query={query}>
{alias => ( {alias => (
<SimplePageBuilder.MultipleSections> <SimplePageBuilder.MultipleSections>
@ -60,7 +76,7 @@ export default function AliasDetailRoute(): ReactElement {
<AliasTypeIndicator type={alias.type} /> <AliasTypeIndicator type={alias.type} />
</Grid> </Grid>
<Grid item> <Grid item>
<AliasAddress address={address} /> <AliasAddress address={`${alias.local}@${alias.domain}`} />
</Grid> </Grid>
<Grid item> <Grid item>
<ChangeAliasActivationStatusSwitch <ChangeAliasActivationStatusSwitch

View File

@ -8,21 +8,32 @@ import {AdminSettings} from "~/server-types"
import {getAdminSettings} from "~/apis" import {getAdminSettings} from "~/apis"
import {QueryResult} from "~/components" import {QueryResult} from "~/components"
import {DEFAULT_ADMIN_SETTINGS} from "~/constants/admin-settings" import {DEFAULT_ADMIN_SETTINGS} from "~/constants/admin-settings"
import SettingsDisabled from "~/route-widgets/GlobalSettingsRoute/SettingsDisabled"
import SettingsForm from "~/route-widgets/GlobalSettingsRoute/SettingsForm" import SettingsForm from "~/route-widgets/GlobalSettingsRoute/SettingsForm"
export default function GlobalSettingsRoute(): ReactElement { export default function GlobalSettingsRoute(): ReactElement {
const queryKey = ["get_admin_settings"] const queryKey = ["get_admin_settings"]
const query = useQuery<AdminSettings, AxiosError>(queryKey, async () => { const query = useQuery<AdminSettings | null, AxiosError>(queryKey, async () => {
const settings = getAdminSettings() const {code, detail, ...settings} = await getAdminSettings()
if (code === "error:settings:global_settings_disabled") {
return null
} else {
return _.mergeWith({}, DEFAULT_ADMIN_SETTINGS, settings, (o, s) => return _.mergeWith({}, DEFAULT_ADMIN_SETTINGS, settings, (o, s) =>
_.isNull(s) ? o : s, _.isNull(s) ? o : s,
) as AdminSettings ) as AdminSettings
}
}) })
return ( return (
<QueryResult<AdminSettings> query={query}> <QueryResult<AdminSettings | null> query={query}>
{settings => <SettingsForm settings={settings} queryKey={queryKey} />} {settings =>
settings === null ? (
<SettingsDisabled />
) : (
<SettingsForm settings={settings} queryKey={queryKey} />
)
}
</QueryResult> </QueryResult>
) )
} }

View File

@ -7,10 +7,15 @@ import {useQuery} from "@tanstack/react-query"
import {List} from "@mui/material" import {List} from "@mui/material"
import {DecryptedReportContent, Report} from "~/server-types" import {DecryptedReportContent, Report} from "~/server-types"
import {getReport} from "~/apis" import {deleteReport, getReport} from "~/apis"
import {DecryptReport, QueryResult, SimpleOverlayInformation, SimplePageBuilder} from "~/components" import {
DecryptReport,
DeleteButton,
QueryResult,
SimpleOverlayInformation,
SimplePageBuilder,
} from "~/components"
import {WithEncryptionRequired} from "~/hocs" import {WithEncryptionRequired} from "~/hocs"
import DeleteButton from "~/route-widgets/ReportDetailRoute/DeleteButton"
import ExpandedUrlsListItem from "~/route-widgets/ReportDetailRoute/ExpandedUrlsListItem" import ExpandedUrlsListItem from "~/route-widgets/ReportDetailRoute/ExpandedUrlsListItem"
import ProxiedImagesListItem from "~/route-widgets/ReportDetailRoute/ProxiedImagesListItem" import ProxiedImagesListItem from "~/route-widgets/ReportDetailRoute/ProxiedImagesListItem"
import SinglePixelImageTrackersListItem from "~/route-widgets/ReportDetailRoute/SinglePixelImageTrackersListItem" import SinglePixelImageTrackersListItem from "~/route-widgets/ReportDetailRoute/SinglePixelImageTrackersListItem"
@ -26,7 +31,18 @@ function ReportDetailRoute(): ReactElement {
return ( return (
<SimplePageBuilder.Page <SimplePageBuilder.Page
title="Report Details" title="Report Details"
actions={query.data && <DeleteButton id={query.data.id} />} actions={
query.data && (
<DeleteButton
onDelete={() => 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")}
/>
)
}
> >
<QueryResult<Report> query={query}> <QueryResult<Report> query={query}>
{encryptedReport => ( {encryptedReport => (

View File

@ -6,9 +6,9 @@ import {AxiosError} from "axios"
import {useQuery} from "@tanstack/react-query" import {useQuery} from "@tanstack/react-query"
import {Grid} from "@mui/material" import {Grid} from "@mui/material"
import {QueryResult, SimplePage, SimplePageBuilder} from "~/components" import {DeleteButton, QueryResult, SimplePage, SimplePageBuilder} from "~/components"
import {ReservedAlias} from "~/server-types" import {ReservedAlias} from "~/server-types"
import {getReservedAlias} from "~/apis" import {deleteReservedAlias, getReservedAlias} from "~/apis"
import AliasActivationSwitch from "~/route-widgets/ReservedAliasDetailRoute/AliasActivationSwitch" import AliasActivationSwitch from "~/route-widgets/ReservedAliasDetailRoute/AliasActivationSwitch"
import AliasAddress from "~/route-widgets/AliasDetailRoute/AliasAddress" import AliasAddress from "~/route-widgets/AliasDetailRoute/AliasAddress"
import AliasUsersList from "~/route-widgets/ReservedAliasDetailRoute/AliasUsersList" import AliasUsersList from "~/route-widgets/ReservedAliasDetailRoute/AliasUsersList"
@ -21,7 +21,25 @@ export default function ReservedAliasDetailRoute(): ReactElement {
const query = useQuery<ReservedAlias, AxiosError>(queryKey, () => getReservedAlias(params.id!)) const query = useQuery<ReservedAlias, AxiosError>(queryKey, () => getReservedAlias(params.id!))
return ( return (
<SimplePage title={t("routes.ReservedAliasDetailRoute.title")}> <SimplePage
title={t("routes.ReservedAliasDetailRoute.title")}
actions={
query.data && (
<DeleteButton
onDelete={() => 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")}
/>
)
}
>
<QueryResult<ReservedAlias, AxiosError> query={query}> <QueryResult<ReservedAlias, AxiosError> query={query}>
{alias => ( {alias => (
<SimplePageBuilder.MultipleSections> <SimplePageBuilder.MultipleSections>

View File

@ -9,7 +9,7 @@ import React, {ReactElement, useContext} from "react"
import {Grid, Paper, Typography, useTheme} from "@mui/material" import {Grid, Paper, Typography, useTheme} from "@mui/material"
import {useMutation} from "@tanstack/react-query" import {useMutation} from "@tanstack/react-query"
import {AuthenticationDetails, ServerSettings} from "~/server-types" import {ServerSettings, ServerUser} from "~/server-types"
import {VerifyEmailData, verifyEmail} from "~/apis" import {VerifyEmailData, verifyEmail} from "~/apis"
import {useQueryParams} from "~/hooks" import {useQueryParams} from "~/hooks"
import {AuthContext} from "~/components" import {AuthContext} from "~/components"
@ -41,15 +41,12 @@ export default function VerifyEmailRoute(): ReactElement {
return token.split("").every(char => chars.includes(char)) return token.split("").every(char => chars.includes(char))
}) })
const {mutateAsync} = useMutation<AuthenticationDetails, AxiosError, VerifyEmailData>( const {mutateAsync} = useMutation<ServerUser, AxiosError, VerifyEmailData>(verifyEmail, {
verifyEmail, onSuccess: user => {
{
onSuccess: ({user}) => {
login(user) login(user)
navigate("/auth/complete-account") navigate("/auth/complete-account")
}, },
}, })
)
const {loading} = useAsync(async () => { const {loading} = useAsync(async () => {
await emailSchema.validate(email) await emailSchema.validate(email)
await tokenSchema.validate(token) await tokenSchema.validate(token)

View File

@ -83,6 +83,7 @@ export interface ServerSettings {
customAliasSuffixLength: number customAliasSuffixLength: number
instanceSalt: string instanceSalt: string
publicKey: string publicKey: string
allowAliasDeletion: boolean
} }
export interface Alias { export interface Alias {
@ -210,4 +211,30 @@ export interface AdminSettings {
userEmailEnableDisposableEmails: boolean userEmailEnableDisposableEmails: boolean
userEmailEnableOtherRelays: boolean | null userEmailEnableOtherRelays: boolean | null
allowStatistics: 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
}
}
} }