diff --git a/public/locales/en-US/translation.json b/public/locales/en-US/translation.json
index 8f0ede7..4ff93a5 100644
--- a/public/locales/en-US/translation.json
+++ b/public/locales/en-US/translation.json
@@ -281,7 +281,28 @@
"description": "We are logging you out..."
},
"AdminRoute": {
- "title": "Site configuration"
+ "title": "Site configuration",
+ "forms": {
+ "reservedAliases": {
+ "title": "Reserved Aliases",
+ "description": "Define which aliases will be reserved for your domain.",
+ "saveAction": "Create Alias",
+ "fields": {
+ "local": {
+ "label": "Local"
+ },
+ "users": {
+ "label": "Users",
+ "me": "{{email}} (Me)"
+ }
+ },
+ "explanation": {
+ "step1": "User from outside",
+ "step2": "Sends mail to",
+ "step4": "KleckRelay forwards to"
+ }
+ }
+ }
}
},
diff --git a/src/App.tsx b/src/App.tsx
index 8771a11..62d9b06 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -102,6 +102,7 @@ const router = createBrowserRouter([
},
{
path: "/admin",
+ loader: getServerSettings,
element: ,
},
],
diff --git a/src/apis/create-reserved-alias.ts b/src/apis/create-reserved-alias.ts
new file mode 100644
index 0000000..28c1b96
--- /dev/null
+++ b/src/apis/create-reserved-alias.ts
@@ -0,0 +1,25 @@
+import {ReservedAlias} from "~/server-types"
+import {client} from "~/constants/axios-client"
+
+export interface CreateReservedAliasData {
+ local: string
+ users: Array<{
+ id: string
+ }>
+
+ isActive?: boolean
+}
+
+export default async function createReservedAlias(
+ aliasData: CreateReservedAliasData,
+): Promise {
+ const {data} = await client.post(
+ `${import.meta.env.VITE_SERVER_BASE_URL}/v1/reserved-alias`,
+ aliasData,
+ {
+ withCredentials: true,
+ },
+ )
+
+ return data
+}
diff --git a/src/apis/index.ts b/src/apis/index.ts
index ddf3005..438a419 100644
--- a/src/apis/index.ts
+++ b/src/apis/index.ts
@@ -42,3 +42,5 @@ export * from "./get-admin-users"
export {default as getAdminUsers} from "./get-admin-users"
export * from "./get-reserved-aliases"
export {default as getReservedAliases} from "./get-reserved-aliases"
+export * from "./create-reserved-alias"
+export {default as createReservedAlias} from "./create-reserved-alias"
diff --git a/src/components/widgets/SimpleForm.tsx b/src/components/widgets/SimpleForm.tsx
index d8524d4..36fc510 100644
--- a/src/components/widgets/SimpleForm.tsx
+++ b/src/components/widgets/SimpleForm.tsx
@@ -73,7 +73,7 @@ export default function SimpleForm({
{children.map(input => (
-
+
{input}
))}
diff --git a/src/route-widgets/AdminPage/AliasExplanation.tsx b/src/route-widgets/AdminPage/AliasExplanation.tsx
new file mode 100644
index 0000000..70d3528
--- /dev/null
+++ b/src/route-widgets/AdminPage/AliasExplanation.tsx
@@ -0,0 +1,99 @@
+import {Grid, List, ListItem, ListItemText, Typography, useTheme} from "@mui/material"
+import {ReactElement} from "react"
+import {MdMail} from "react-icons/md"
+import {useLoaderData} from "react-router"
+import {ServerSettings} from "~/server-types"
+import {useTranslation} from "react-i18next"
+import {BsArrowDown} from "react-icons/bs"
+import {FaMask} from "react-icons/fa"
+import {HiUsers} from "react-icons/hi"
+
+export interface AliasExplanationProps {
+ local: string
+ emails: string[]
+}
+
+export default function AliasExplanation({local, emails}: AliasExplanationProps): ReactElement {
+ const {t} = useTranslation()
+ const theme = useTheme()
+ const serverSettings = useLoaderData() as ServerSettings
+
+ return (
+
+
+
+
+
+
+
+
+ {t("routes.AdminRoute.forms.reservedAliases.explanation.step1")}
+
+
+
+
+
+
+
+
+
+
+
+ {t("routes.AdminRoute.forms.reservedAliases.explanation.step2")}
+
+
+
+
+
+
+
+
+
+
+
+ {local}
+ @{serverSettings.mailDomain}
+
+
+
+
+
+
+
+
+
+
+
+ {t("routes.AdminRoute.forms.reservedAliases.explanation.step4")}
+
+
+
+
+
+
+
+
+
+
+
+ {emails.map(email => (
+
+
+
+ ))}
+
+
+
+
+
+ )
+}
diff --git a/src/route-widgets/AdminPage/ReservedAliasesForm.tsx b/src/route-widgets/AdminPage/ReservedAliasesForm.tsx
index 86fac79..e8309ed 100644
--- a/src/route-widgets/AdminPage/ReservedAliasesForm.tsx
+++ b/src/route-widgets/AdminPage/ReservedAliasesForm.tsx
@@ -1,17 +1,182 @@
+import * as yup from "yup"
import {ReactElement} from "react"
import {AxiosError} from "axios"
+import {useTranslation} from "react-i18next"
+import {useFormik} from "formik"
-import {useQuery} from "@tanstack/react-query"
+import {useMutation, useQuery} from "@tanstack/react-query"
-import {GetAdminUsersResponse, getAdminUsers} from "~/apis"
+import {
+ CreateReservedAliasData,
+ GetAdminUsersResponse,
+ createReservedAlias,
+ getAdminUsers,
+} from "~/apis"
+import {Grid, InputAdornment, MenuItem, TextField} from "@mui/material"
+import {BiText} from "react-icons/bi"
+import {HiUsers} from "react-icons/hi"
+import {useErrorSuccessSnacks, useNavigateToNext, useUser} from "~/hooks"
+import {ReservedAlias, ServerUser} from "~/server-types"
+import {parseFastAPIError} from "~/utils"
+import {SimpleForm} from "~/components"
+import AliasExplanation from "~/route-widgets/AdminPage/AliasExplanation"
+
+interface Form {
+ local: string
+ users: string[]
+
+ isActive?: boolean
+
+ nonFieldError?: string
+}
export interface ReservedAliasesFormProps {}
export default function ReservedAliasesForm({}: ReservedAliasesFormProps): ReactElement {
+ const {t} = useTranslation()
+ const meUser = useUser()
+ const {showError, showSuccess} = useErrorSuccessSnacks()
+ const navigateToNext = useNavigateToNext("/admin/reserved-aliases")
const {data: {users} = {}} = useQuery(
["getAdminUsers"],
getAdminUsers,
)
+ const {mutateAsync: createAlias} = useMutation<
+ ReservedAlias,
+ AxiosError,
+ CreateReservedAliasData
+ >(createReservedAlias, {
+ onSuccess: () => {
+ showSuccess(t("relations.alias.mutations.success.aliasCreation"))
+ navigateToNext()
+ },
+ })
- console.log(users)
+ const schema = yup.object().shape({
+ local: yup
+ .string()
+ .required()
+ .label(t("routes.AdminRoute.forms.reservedAliases.fields.local.label")),
+ isActive: yup
+ .boolean()
+ .label(t("routes.AdminRoute.forms.reservedAliases.fields.isActive.label")),
+ // Only store IDs of users, as they provide a reference to the user
+ users: yup
+ .array()
+ .of(yup.string())
+ .label(t("routes.AdminRoute.forms.reservedAliases.fields.users.label")),
+ })
+ const formik = useFormik