added Reserved Alias creation

This commit is contained in:
Myzel394 2023-02-04 12:12:33 +01:00
parent cd1fe2005a
commit 32f8d17418
7 changed files with 318 additions and 5 deletions

View File

@ -281,7 +281,28 @@
"description": "We are logging you out..." "description": "We are logging you out..."
}, },
"AdminRoute": { "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"
}
}
}
} }
}, },

View File

@ -102,6 +102,7 @@ const router = createBrowserRouter([
}, },
{ {
path: "/admin", path: "/admin",
loader: getServerSettings,
element: <AdminRoute />, element: <AdminRoute />,
}, },
], ],

View File

@ -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<ReservedAlias> {
const {data} = await client.post(
`${import.meta.env.VITE_SERVER_BASE_URL}/v1/reserved-alias`,
aliasData,
{
withCredentials: true,
},
)
return data
}

View File

@ -42,3 +42,5 @@ export * from "./get-admin-users"
export {default as getAdminUsers} from "./get-admin-users" export {default as getAdminUsers} from "./get-admin-users"
export * from "./get-reserved-aliases" export * from "./get-reserved-aliases"
export {default as getReservedAliases} 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"

View File

@ -73,7 +73,7 @@ export default function SimpleForm({
<Grid item> <Grid item>
<Grid container spacing={3} direction="column" alignItems="center"> <Grid container spacing={3} direction="column" alignItems="center">
{children.map(input => ( {children.map(input => (
<Grid item key={input.key}> <Grid item key={input.key} width="100%">
{input} {input}
</Grid> </Grid>
))} ))}

View File

@ -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 (
<Grid
container
direction="column"
padding={4}
gap={4}
borderRadius={theme.shape.borderRadius}
border={1}
borderColor={theme.palette.text.disabled}
bgcolor={theme.palette.background.default}
>
<Grid item>
<Grid container direction="column" spacing={1} alignItems="center">
<Grid item>
<MdMail size={24} />
</Grid>
<Grid item>
<Typography variant="caption" align="center">
{t("routes.AdminRoute.forms.reservedAliases.explanation.step1")}
</Typography>
</Grid>
</Grid>
</Grid>
<Grid item>
<Grid container direction="column" spacing={1} alignItems="center">
<Grid item>
<BsArrowDown size={24} />
</Grid>
<Grid item>
<Typography variant="caption" align="center">
{t("routes.AdminRoute.forms.reservedAliases.explanation.step2")}
</Typography>
</Grid>
</Grid>
</Grid>
<Grid item>
<Grid container direction="column" spacing={1} alignItems="center">
<Grid item>
<FaMask size={24} />
</Grid>
<Grid item>
<Typography variant="body1" align="center">
<span style={{display: "block"}}>{local}</span>
<span style={{opacity: 0.4}}>@{serverSettings.mailDomain}</span>
</Typography>
</Grid>
</Grid>
</Grid>
<Grid item>
<Grid container direction="column" spacing={1} alignItems="center">
<Grid item>
<BsArrowDown size={24} />
</Grid>
<Grid item>
<Typography variant="caption" align="center">
{t("routes.AdminRoute.forms.reservedAliases.explanation.step4")}
</Typography>
</Grid>
</Grid>
</Grid>
<Grid item>
<Grid container direction="column" spacing={1} alignItems="center">
<Grid item>
<HiUsers size={24} />
</Grid>
<Grid item>
<List dense>
{emails.map(email => (
<ListItem key={email}>
<ListItemText primary={email} />
</ListItem>
))}
</List>
</Grid>
</Grid>
</Grid>
</Grid>
)
}

View File

@ -1,17 +1,182 @@
import * as yup from "yup"
import {ReactElement} from "react" import {ReactElement} from "react"
import {AxiosError} from "axios" 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 interface ReservedAliasesFormProps {}
export default function ReservedAliasesForm({}: ReservedAliasesFormProps): ReactElement { 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<GetAdminUsersResponse, AxiosError>( const {data: {users} = {}} = useQuery<GetAdminUsersResponse, AxiosError>(
["getAdminUsers"], ["getAdminUsers"],
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<Form>({
validationSchema: schema,
initialValues: {
local: "",
users: [],
},
onSubmit: async (values, {setErrors, resetForm}) => {
try {
await createAlias({
local: values.local,
users: values.users.map(id => ({
id,
})),
})
} catch (error) {
setErrors(parseFastAPIError(error as AxiosError))
}
},
})
const getUser = (id: string) => users?.find(user => user.id === id) as any as ServerUser
if (!users) return null
return (
<Grid container spacing={4} flexDirection="column" alignItems="center">
<Grid item>
<form onSubmit={formik.handleSubmit}>
<SimpleForm
title={t("routes.AdminRoute.forms.reservedAliases.title")}
description={t("routes.AdminRoute.forms.reservedAliases.description")}
isSubmitting={formik.isSubmitting}
continueActionLabel={t(
"routes.AdminRoute.forms.reservedAliases.saveAction",
)}
nonFieldError={formik.errors.nonFieldError}
>
{[
<TextField
key="local"
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<BiText />
</InputAdornment>
),
}}
name="local"
id="local"
label={t(
"routes.AdminRoute.forms.reservedAliases.fields.local.label",
)}
value={formik.values.local}
onChange={formik.handleChange}
disabled={formik.isSubmitting}
error={formik.touched.local && Boolean(formik.errors.local)}
helperText={formik.touched.local && formik.errors.local}
/>,
<TextField
key="users"
fullWidth
select
InputProps={{
startAdornment: (
<InputAdornment position="start">
<HiUsers />
</InputAdornment>
),
}}
name="users"
id="users"
label={t(
"routes.AdminRoute.forms.reservedAliases.fields.users.label",
)}
SelectProps={{
multiple: true,
value: formik.values.users,
onChange: formik.handleChange,
}}
disabled={formik.isSubmitting}
error={formik.touched.users && Boolean(formik.errors.users)}
helperText={formik.touched.users && formik.errors.users}
>
{users.map(user => (
<MenuItem key={user.id} value={user.id}>
{(() => {
// Check if user is me
if (user.id === meUser.id) {
return t(
"routes.AdminRoute.forms.reservedAliases.fields.users.me",
{
email: user.email.address,
},
)
}
return user.email.address
})()}
</MenuItem>
))}
</TextField>,
]}
</SimpleForm>
</form>
</Grid>
<Grid item>
<AliasExplanation
local={formik.values.local}
emails={formik.values.users.map(userId => getUser(userId).email.address)}
/>
</Grid>
</Grid>
)
} }