mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-21 08:40:32 +02:00
feat(settings): Add admin settings page (draft)
This commit is contained in:
parent
6a1629114c
commit
e6cdfcbc5a
@ -313,7 +313,8 @@
|
|||||||
"AdminRoute": {
|
"AdminRoute": {
|
||||||
"title": "Site configuration",
|
"title": "Site configuration",
|
||||||
"routes": {
|
"routes": {
|
||||||
"reservedAliases": "Reserved Aliases"
|
"reservedAliases": "Reserved Aliases",
|
||||||
|
"settings": "Global Settings"
|
||||||
},
|
},
|
||||||
"forms": {
|
"forms": {
|
||||||
"reservedAliases": {
|
"reservedAliases": {
|
||||||
@ -334,6 +335,48 @@
|
|||||||
"step2": "Sends mail to",
|
"step2": "Sends mail to",
|
||||||
"step4": "KleckRelay forwards to"
|
"step4": "KleckRelay forwards to"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"randomEmailIdMinLength": {
|
||||||
|
"label": "Minimum random alias ID length",
|
||||||
|
"description": "The minimum length for randomly generated emails. The server will automatically increase the length if required so."
|
||||||
|
},
|
||||||
|
"randomEmailIdChars": {
|
||||||
|
"label": "Random alias character pool",
|
||||||
|
"description": "Characters that are used to generate random emails."
|
||||||
|
},
|
||||||
|
"randomEmailIdLengthIncreaseOnPercentage": {
|
||||||
|
"label": "Percentage of used aliases",
|
||||||
|
"description": "If the percentage of used random email IDs is higher than this value, the length of the random email ID will be increased. This is used to prevent spammers from guessing the email ID."
|
||||||
|
},
|
||||||
|
"customEmailSuffixLength": {
|
||||||
|
"label": "Custom email suffix length",
|
||||||
|
"description": "The length of the custom email suffix."
|
||||||
|
},
|
||||||
|
"customEmailSuffixChars": {
|
||||||
|
"label": "Custom email suffix character pool",
|
||||||
|
"description": "Characters that are used to generate custom email suffixes."
|
||||||
|
},
|
||||||
|
"imageProxyStorageLifeTimeInHours": {
|
||||||
|
"label": "Image proxy storage lifetime",
|
||||||
|
"description": "The lifetime of images that are stored on the server. After this time, the image will be deleted."
|
||||||
|
},
|
||||||
|
"enableImageProxy": {
|
||||||
|
"label": "Enable image proxy",
|
||||||
|
"description": "If enabled, images will be stored on the server and forwarded to the user. This is useful if you want to prevent the user from seeing the IP address of the server. This will only affect new images."
|
||||||
|
},
|
||||||
|
"userEmailEnableDisposableEmails": {
|
||||||
|
"label": "Enable disposable emails for new accounts",
|
||||||
|
"description": "If enabled, users will be able to use disposable emails when creating a new account. This will only affect new accounts."
|
||||||
|
},
|
||||||
|
"userEmailEnableOtherRelays": {
|
||||||
|
"label": "Enable other relays for new accounts",
|
||||||
|
"description": "If enabled, users will be able to use other relays (such as SimpleLogin or DuckDuckGo's Email Tracking Protection) when creating a new account. This will only affect new accounts."
|
||||||
|
},
|
||||||
|
"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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import AuthenticatedRoute from "~/routes/AuthenticatedRoute"
|
|||||||
import CompleteAccountRoute from "~/routes/CompleteAccountRoute"
|
import CompleteAccountRoute from "~/routes/CompleteAccountRoute"
|
||||||
import CreateReservedAliasRoute from "~/routes/CreateReservedAliasRoute"
|
import CreateReservedAliasRoute from "~/routes/CreateReservedAliasRoute"
|
||||||
import EnterDecryptionPassword from "~/routes/EnterDecryptionPassword"
|
import EnterDecryptionPassword from "~/routes/EnterDecryptionPassword"
|
||||||
|
import GlobalSettingsRoute from "~/routes/GlobalSettingsRoute"
|
||||||
import I18nHandler from "./I18nHandler"
|
import I18nHandler from "./I18nHandler"
|
||||||
import LoginRoute from "~/routes/LoginRoute"
|
import LoginRoute from "~/routes/LoginRoute"
|
||||||
import LogoutRoute from "~/routes/LogoutRoute"
|
import LogoutRoute from "~/routes/LogoutRoute"
|
||||||
@ -119,6 +120,10 @@ const router = createBrowserRouter([
|
|||||||
loader: getServerSettings,
|
loader: getServerSettings,
|
||||||
element: <CreateReservedAliasRoute />,
|
element: <CreateReservedAliasRoute />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/admin/settings",
|
||||||
|
element: <GlobalSettingsRoute />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
10
src/apis/get-admin-settings.ts
Normal file
10
src/apis/get-admin-settings.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import {client} from "~/constants/axios-client"
|
||||||
|
import {AdminSettings} from "~/server-types"
|
||||||
|
|
||||||
|
export default async function getAdminSettings(): Promise<AdminSettings> {
|
||||||
|
const {data} = await client.get(`${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/settings`, {
|
||||||
|
withCredentials: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
@ -48,3 +48,7 @@ export * from "./get-reserved-alias"
|
|||||||
export {default as getReservedAlias} from "./get-reserved-alias"
|
export {default as getReservedAlias} from "./get-reserved-alias"
|
||||||
export * from "./update-reserved-alias"
|
export * from "./update-reserved-alias"
|
||||||
export {default as updateReservedAlias} from "./update-reserved-alias"
|
export {default as updateReservedAlias} from "./update-reserved-alias"
|
||||||
|
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"
|
||||||
|
16
src/apis/update-admin-settings.ts
Normal file
16
src/apis/update-admin-settings.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import {client} from "~/constants/axios-client"
|
||||||
|
import {AdminSettings} from "~/server-types"
|
||||||
|
|
||||||
|
export default async function updateAdminSettings(
|
||||||
|
settings: Partial<AdminSettings>,
|
||||||
|
): Promise<AdminSettings> {
|
||||||
|
const {data} = await client.patch(
|
||||||
|
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/admin/settings`,
|
||||||
|
settings,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
92
src/route-widgets/GlobalSettingsRoute/SettingForm.tsx
Normal file
92
src/route-widgets/GlobalSettingsRoute/SettingForm.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import * as yup from "yup"
|
||||||
|
import {AdminSettings} from "~/server-types"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
import {useFormik} from "formik"
|
||||||
|
import {SimpleForm} from "~/components"
|
||||||
|
import {TextField} from "@mui/material"
|
||||||
|
|
||||||
|
export interface SettingsFormProps {
|
||||||
|
settings: AdminSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SettingsForm({settings}: SettingsFormProps) {
|
||||||
|
const {t} = useTranslation()
|
||||||
|
|
||||||
|
const validationSchema = yup.object().shape({
|
||||||
|
randomEmailIdMinLength: yup
|
||||||
|
.number()
|
||||||
|
.min(1)
|
||||||
|
.max(1_023)
|
||||||
|
.label(t("routes.AdminRoute.forms.settings.randomEmailIdMinLength.label")),
|
||||||
|
randomEmailIdChars: yup
|
||||||
|
.string()
|
||||||
|
.label(t("routes.AdminRoute.forms.settings.randomEmailIdChars.label")),
|
||||||
|
randomEmailLengthIncreaseOnPercentage: yup
|
||||||
|
.number()
|
||||||
|
.label(
|
||||||
|
t("routes.AdminRoute.forms.settings.randomEmailLengthIncreaseOnPercentage.label"),
|
||||||
|
),
|
||||||
|
imageProxyStorageLifeTimeInHours: yup
|
||||||
|
.number()
|
||||||
|
.label(t("routes.AdminRoute.forms.settings.imageProxyStorageLifeTimeInHours.label")),
|
||||||
|
customEmailSuffixLength: yup
|
||||||
|
.number()
|
||||||
|
.min(1)
|
||||||
|
.max(1_023)
|
||||||
|
.label(t("routes.AdminRoute.forms.settings.customEmailSuffixLength-label")),
|
||||||
|
customEmailSuffixChars: yup
|
||||||
|
.string()
|
||||||
|
.label(t("routes.AdminRoute.forms.settings.customEmailSuffixChars.label")),
|
||||||
|
userEmailEnableDisposableEmails: yup
|
||||||
|
.boolean()
|
||||||
|
.label(t("routes.AdminRoute.forms.settings.userEmailEnableDisposableEmails.label")),
|
||||||
|
userEmailEnableOtherRelays: yup
|
||||||
|
.boolean()
|
||||||
|
.label(t("routes.AdminRoute.forms.settings.userEmailEnableOtherRelays.label")),
|
||||||
|
enableImageProxy: yup
|
||||||
|
.boolean()
|
||||||
|
.label(t("routes.AdminRoute.forms.settings.enableImageProxy.label")),
|
||||||
|
allowStatistics: yup
|
||||||
|
.boolean()
|
||||||
|
.label(t("routes.AdminRoute.forms.settings.allowStatistics.label")),
|
||||||
|
})
|
||||||
|
|
||||||
|
const formik = useFormik<AdminSettings>({
|
||||||
|
validationSchema,
|
||||||
|
onSubmit: console.log,
|
||||||
|
initialValues: settings,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={formik.handleSubmit}>
|
||||||
|
<SimpleForm
|
||||||
|
isSubmitting={formik.isSubmitting}
|
||||||
|
title={t("routes.AdminRoute.settings.title")}
|
||||||
|
description={t("routes.AdminRoute.settings.description")}
|
||||||
|
continueActionLabel={t("general.save")}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
<TextField
|
||||||
|
key="random_email_id_min_length"
|
||||||
|
label={t("routes.AdminRoute.forms.settings.randomEmailIdMinLength.label")}
|
||||||
|
name="randomEmailIdMinLength"
|
||||||
|
value={formik.values.randomEmailIdMinLength}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
error={
|
||||||
|
formik.touched.randomEmailIdMinLength &&
|
||||||
|
Boolean(formik.errors.randomEmailIdMinLength)
|
||||||
|
}
|
||||||
|
helperText={
|
||||||
|
(formik.touched.randomEmailIdMinLength &&
|
||||||
|
formik.errors.randomEmailIdMinLength) ||
|
||||||
|
t("routes.AdminRoute.forms.settings.randomEmailIdMinLength.description")
|
||||||
|
}
|
||||||
|
type="number"
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
inputMode="numeric"
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
</SimpleForm>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import {ReactElement, useLayoutEffect} from "react"
|
import {ReactElement, useLayoutEffect} from "react"
|
||||||
import {useTranslation} from "react-i18next"
|
import {useTranslation} from "react-i18next"
|
||||||
import {BsStarFill} from "react-icons/bs"
|
import {BsStarFill} from "react-icons/bs"
|
||||||
|
import {AiFillEdit} from "react-icons/ai"
|
||||||
import {Link} from "react-router-dom"
|
import {Link} from "react-router-dom"
|
||||||
|
|
||||||
import {List, ListItemButton, ListItemIcon, ListItemText} from "@mui/material"
|
import {List, ListItemButton, ListItemIcon, ListItemText} from "@mui/material"
|
||||||
@ -13,6 +14,8 @@ export default function AdminRoute(): ReactElement {
|
|||||||
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()
|
||||||
@ -28,6 +31,12 @@ export default function AdminRoute(): ReactElement {
|
|||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={t("routes.AdminRoute.routes.reservedAliases")} />
|
<ListItemText primary={t("routes.AdminRoute.routes.reservedAliases")} />
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
|
<ListItemButton component={Link} to="/admin/settings">
|
||||||
|
<ListItemIcon>
|
||||||
|
<AiFillEdit />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary={t("routes.AdminRoute.routes.settings")} />
|
||||||
|
</ListItemButton>
|
||||||
</List>
|
</List>
|
||||||
</SimplePageBuilder.Page>
|
</SimplePageBuilder.Page>
|
||||||
)
|
)
|
||||||
|
19
src/routes/GlobalSettingsRoute.tsx
Normal file
19
src/routes/GlobalSettingsRoute.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import {ReactElement} from "react"
|
||||||
|
import {useQuery} from "@tanstack/react-query"
|
||||||
|
import {AdminSettings} from "~/server-types"
|
||||||
|
import {AxiosError} from "axios"
|
||||||
|
import {getAdminSettings} from "~/apis"
|
||||||
|
import {QueryResult} from "~/components"
|
||||||
|
import {useTranslation} from "react-i18next"
|
||||||
|
import SettingsForm from "~/route-widgets/GlobalSettingsRoute/SettingForm"
|
||||||
|
|
||||||
|
export default function GlobalSettingsRoute(): ReactElement {
|
||||||
|
const {t} = useTranslation()
|
||||||
|
const query = useQuery<AdminSettings, AxiosError>(["get_admin_settings"], getAdminSettings)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryResult<AdminSettings> query={query}>
|
||||||
|
{settings => <SettingsForm settings={settings} />}
|
||||||
|
</QueryResult>
|
||||||
|
)
|
||||||
|
}
|
@ -198,3 +198,16 @@ export interface GetPageData {
|
|||||||
page?: number
|
page?: number
|
||||||
size?: number
|
size?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminSettings {
|
||||||
|
randomEmailIdMinLength: number
|
||||||
|
randomEmailIdChars: string
|
||||||
|
randomEmailLengthIncreaseOnPercentage: number
|
||||||
|
customEmailSuffixLength: number
|
||||||
|
customEmailSuffixChars: string
|
||||||
|
imageProxyStorageLifeTimeInHours: number
|
||||||
|
enableImageProxy: boolean
|
||||||
|
userEmailEnableDisposableEmails: boolean
|
||||||
|
userEmailEnableOtherRelays: boolean
|
||||||
|
allowStatistics: boolean
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user