mirror of
https://github.com/Myzel394/kleckrelay-website.git
synced 2025-06-20 08:15:26 +02:00
added Reserved Alias creation
This commit is contained in:
parent
cd1fe2005a
commit
32f8d17418
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -102,6 +102,7 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/admin",
|
path: "/admin",
|
||||||
|
loader: getServerSettings,
|
||||||
element: <AdminRoute />,
|
element: <AdminRoute />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
25
src/apis/create-reserved-alias.ts
Normal file
25
src/apis/create-reserved-alias.ts
Normal 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
|
||||||
|
}
|
@ -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"
|
||||||
|
@ -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>
|
||||||
))}
|
))}
|
||||||
|
99
src/route-widgets/AdminPage/AliasExplanation.tsx
Normal file
99
src/route-widgets/AdminPage/AliasExplanation.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user