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...",
"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": {

View File

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

View File

@ -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<string, unknown> = {
"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 {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`, {
withCredentials: true,
})

View File

@ -1,8 +1,8 @@
import {client} from "~/constants/axios-client"
import {Alias} from "~/server-types"
export default async function getAlias(address: string): Promise<Alias> {
const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/alias/${address}`, {
export default async function getAlias(aliasID: string): Promise<Alias> {
const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/alias/${aliasID}`, {
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"
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`, {
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 * 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 {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<RefreshTokenResult> {
export default async function refreshToken(): Promise<ServerUser> {
const {data} = await client.post(
REFRESH_TOKEN_URL,
{},

View File

@ -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

View File

@ -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<SimpleDetailResponse> {
): Promise<ResendEmailVerificationCodeResponse> {
const {data} = await client.post(
`${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 parseUser from "~/apis/helpers/parse-user"
@ -9,10 +9,8 @@ export interface UpdateAccountData {
language?: Language
}
export default async function updateAccount(
updateData: UpdateAccountData,
): Promise<AuthenticationDetails> {
const {data} = await client.patch(
export default async function updateAccount(updateData: UpdateAccountData): Promise<ServerUser> {
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)
}

View File

@ -1,9 +1,15 @@
import {client} from "~/constants/axios-client"
import {AdminSettings} from "~/server-types"
export type UpdateAdminSettingsResponse =
| Partial<AdminSettings> & {
detail: string
code: "error:settings:global_settings_disabled"
}
export default async function updateAdminSettings(
settings: Partial<AdminSettings>,
): Promise<AdminSettings> {
): Promise<UpdateAdminSettingsResponse> {
const {data} = await client.patch(
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/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 parseUser from "~/apis/helpers/parse-user"
@ -7,11 +7,8 @@ export interface VerifyEmailData {
token: string
}
export default async function verifyEmail({
email,
token,
}: VerifyEmailData): Promise<AuthenticationDetails> {
const {data} = await client.post(
export default async function verifyEmail({email, token}: VerifyEmailData): Promise<ServerUser> {
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)
}

View File

@ -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<AuthenticationDetails> {
const {data} = await client.post(
}: VerifyLoginWithEmailData): Promise<ServerUser> {
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)
}

View File

@ -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<RefreshTokenResult, AxiosError, void>(refreshToken, {
onError: () => logout(),
const {mutateAsync: refresh} = useMutation<ServerUser, AxiosError, void>(refreshToken, {
onError: logout,
})
useQuery<AuthenticationDetails, AxiosError>(["get_me"], getMe, {
useQuery<ServerUser, AxiosError>(["get_me"], getMe, {
refetchOnWindowFocus: "always",
refetchOnReconnect: "always",
retry: 2,

View File

@ -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<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 {showError, showSuccess} = useErrorSuccessSnacks()
const navigate = useNavigate()
const {mutate} = useMutation<SimpleDetailResponse, AxiosError, void>(() => deleteReport(id), {
const {mutate} = useMutation<void, AxiosError, void>(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={<MdDelete />}
onClick={() => setShowDeleteDialog(true)}
>
{t("routes.ReportDetailRoute.actions.delete.label")}
{label}
</Button>
<Dialog open={showDeleteDialog} onClose={() => setShowDeleteDialog(false)}>
<DialogTitle>{t("routes.ReportDetailRoute.actions.delete.label")}</DialogTitle>
<DialogTitle>{label}</DialogTitle>
<DialogContent>
<DialogContentText>
{t("routes.ReportDetailRoute.actions.delete.description")}
</DialogContentText>
{description && <DialogContentText>{description}</DialogContentText>}
<DialogContentText color="error">
{t("general.actionNotUndoable")}
</DialogContentText>
@ -69,7 +78,7 @@ export default function ReportDetailRoute({id}: DeleteButtonProps): ReactElement
color="error"
onClick={() => mutate()}
>
{t("routes.ReportDetailRoute.actions.delete.continueAction")}
{continueLabel}
</Button>
</DialogActions>
</Dialog>

View File

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

View File

@ -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"

View File

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

View File

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

View File

@ -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<AuthenticationDetails, AxiosError, UpdateAccountData>(
updateAccount,
)
const {mutateAsync} = useMutation<ServerUser, AxiosError, UpdateAccountData>(updateAccount)
const formik = useFormik<Form>({
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()

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 {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<AdminSettings, AxiosError, Partial<AdminSettings>>(
const {mutateAsync} = useMutation<
UpdateAdminSettingsResponse,
AxiosError,
Partial<AdminSettings>
>(
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<AdminSettings>(queryKey, newSettings)
queryClient.setQueryData<Partial<AdminSettings>>(queryKey, newSettings)
},
},
)
@ -488,6 +496,35 @@ export default function SettingsForm({settings, queryKey}: SettingsFormProps) {
</FormHelperText>
</FormGroup>
</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 item>

View File

@ -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<AuthenticationDetails, AxiosError, VerifyLoginWithEmailData>(
const {mutateAsync} = useMutation<ServerUser, AxiosError, VerifyLoginWithEmailData>(
verifyLoginWithEmail,
{
onSuccess: ({user}) => onConfirm(user),
onSuccess: onConfirm,
},
)
const formik = useFormik<Form>({

View File

@ -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<SimpleDetailResponse, AxiosError, void>(() =>
const mutation = useMutation<ResendEmailLoginCodeResponse, AxiosError, void>(() =>
resendEmailLoginCode({
email,
sameRequestToken,

View File

@ -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<AuthenticationDetails, AxiosError, void>(
const {mutate, isLoading, isError} = useMutation<ServerUser, AxiosError, void>(
() =>
verifyLoginWithEmail({
email,
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 {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<SimpleDetailResponse, AxiosError, void>(() =>
resendEmailVerificationCode(email),
const mutation = useMutation<ResendEmailVerificationCodeResponse, AxiosError, void>(
() => resendEmailVerificationCode(email),
{
onSuccess: ({code}: any) => {
if (code === "ok:email_already_verified") {
onEmailAlreadyVerified()
}
},
},
)
const {mutate} = mutation

View File

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

View File

@ -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 (
<SimplePageBuilder.Page title={t("routes.AdminRoute.title")}>
<ServerStatus />
<List>
<ListItemButton component={Link} to="/admin/reserved-aliases">
<ListItemIcon>

View File

@ -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<Alias | DecryptedAlias, AxiosError>(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 (
<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}>
{alias => (
<SimplePageBuilder.MultipleSections>
@ -60,7 +76,7 @@ export default function AliasDetailRoute(): ReactElement {
<AliasTypeIndicator type={alias.type} />
</Grid>
<Grid item>
<AliasAddress address={address} />
<AliasAddress address={`${alias.local}@${alias.domain}`} />
</Grid>
<Grid item>
<ChangeAliasActivationStatusSwitch

View File

@ -8,21 +8,32 @@ import {AdminSettings} from "~/server-types"
import {getAdminSettings} from "~/apis"
import {QueryResult} from "~/components"
import {DEFAULT_ADMIN_SETTINGS} from "~/constants/admin-settings"
import SettingsDisabled from "~/route-widgets/GlobalSettingsRoute/SettingsDisabled"
import SettingsForm from "~/route-widgets/GlobalSettingsRoute/SettingsForm"
export default function GlobalSettingsRoute(): ReactElement {
const queryKey = ["get_admin_settings"]
const query = useQuery<AdminSettings, AxiosError>(queryKey, async () => {
const settings = getAdminSettings()
const query = useQuery<AdminSettings | null, AxiosError>(queryKey, async () => {
const {code, detail, ...settings} = await getAdminSettings()
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 (
<QueryResult<AdminSettings> query={query}>
{settings => <SettingsForm settings={settings} queryKey={queryKey} />}
<QueryResult<AdminSettings | null> query={query}>
{settings =>
settings === null ? (
<SettingsDisabled />
) : (
<SettingsForm settings={settings} queryKey={queryKey} />
)
}
</QueryResult>
)
}

View File

@ -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 (
<SimplePageBuilder.Page
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}>
{encryptedReport => (

View File

@ -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<ReservedAlias, AxiosError>(queryKey, () => getReservedAlias(params.id!))
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}>
{alias => (
<SimplePageBuilder.MultipleSections>

View File

@ -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<AuthenticationDetails, AxiosError, VerifyEmailData>(
verifyEmail,
{
onSuccess: ({user}) => {
const {mutateAsync} = useMutation<ServerUser, AxiosError, VerifyEmailData>(verifyEmail, {
onSuccess: user => {
login(user)
navigate("/auth/complete-account")
},
},
)
})
const {loading} = useAsync(async () => {
await emailSchema.validate(email)
await tokenSchema.validate(token)

View File

@ -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
}
}
}