#1616 better user management (#1748)

This commit is contained in:
Manuel
2023-12-20 10:18:24 +01:00
committed by GitHub
parent 199b711324
commit 553fa98e61
11 changed files with 760 additions and 127 deletions

View File

@@ -0,0 +1,81 @@
import { Box, Button, Group, TextInput, Title } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import { IconAt, IconCheck, IconLetterCase } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { z } from 'zod';
import { api } from '~/utils/api';
export const ManageUserGeneralForm = ({
userId,
defaultUsername,
defaultEmail,
}: {
userId: string;
defaultUsername: string;
defaultEmail: string;
}) => {
const form = useForm({
initialValues: {
username: defaultUsername,
eMail: defaultEmail,
},
validate: zodResolver(
z.object({
username: z.string(),
eMail: z.string().email().or(z.literal('')),
})
),
validateInputOnBlur: true,
validateInputOnChange: true,
});
const { t } = useTranslation(['manage/users/edit', 'common']);
const utils = api.useUtils();
const { mutate, isLoading } = api.user.updateDetails.useMutation({
onSettled: async () => {
await utils.user.invalidate();
form.resetDirty();
},
});
function handleSubmit() {
mutate({
userId: userId,
username: form.values.username,
eMail: form.values.eMail,
});
}
return (
<Box maw={500}>
<Title order={3}>{t('sections.general.title')}</Title>
<form onSubmit={form.onSubmit(handleSubmit)}>
<TextInput
icon={<IconLetterCase size="1rem" />}
label={t('sections.general.inputs.username.label')}
mb="md"
withAsterisk
{...form.getInputProps('username')}
/>
<TextInput
icon={<IconAt size="1rem" />}
label={t('sections.general.inputs.eMail.label')}
{...form.getInputProps('eMail')}
/>
<Group position="right" mt="md">
<Button
disabled={!form.isDirty() || !form.isValid() || isLoading}
loading={isLoading}
leftIcon={<IconCheck size="1rem" />}
color="green"
variant="light"
type="submit"
>
{t('common:save')}
</Button>
</Group>
</form>
</Box>
);
};

View File

@@ -0,0 +1,83 @@
import { Box, Button, Checkbox, Group, LoadingOverlay, PasswordInput, Title } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import { IconTextSize, IconTrash } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { z } from 'zod';
import { api } from '~/utils/api';
export const ManageUserDanger = ({
userId,
username,
}: {
userId: string;
username: string | null;
}) => {
const form = useForm({
initialValues: {
username: '',
confirm: false,
},
validate: zodResolver(
z.object({
username: z.literal(username),
confirm: z.literal(true),
})
),
validateInputOnBlur: true,
validateInputOnChange: true,
});
const apiUtils = api.useUtils();
const { mutate, isLoading } = api.user.deleteUser.useMutation({
onSuccess: () => {
window.location.href = '/manage/users';
},
onSettled: () => {
void apiUtils.user.details.invalidate();
form.reset();
},
});
const { t } = useTranslation(['manage/users/edit', 'common']);
const handleSubmit = () => {
mutate({
id: userId,
});
};
return (
<Box maw={500}>
<LoadingOverlay visible={isLoading} />
<Title order={3}>{t('sections.deletion.title')}</Title>
<form onSubmit={form.onSubmit(handleSubmit)}>
<PasswordInput
icon={<IconTextSize size="1rem" />}
label={t('sections.deletion.inputs.confirmUsername.label')}
description={t('sections.deletion.inputs.confirmUsername.description')}
mb="md"
withAsterisk
{...form.getInputProps('username')}
/>
<Checkbox
label={t('sections.deletion.inputs.confirm.label')}
description={t('sections.deletion.inputs.confirm.description')}
{...form.getInputProps('confirm')}
/>
<Group position="right" mt="md">
<Button
disabled={!form.isDirty() || !form.isValid()}
leftIcon={<IconTrash size="1rem" />}
loading={isLoading}
color="red"
variant="light"
type="submit"
>
{t('common:delete')}
</Button>
</Group>
</form>
</Box>
);
};

View File

@@ -0,0 +1,65 @@
import { ActionIcon, Badge, Box, Group, Title, Text, Tooltip, Button } from '@mantine/core';
import { openRoleChangeModal } from '~/components/Manage/User/change-user-role.modal';
import { IconUserDown, IconUserUp } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useSession } from 'next-auth/react';
export const ManageUserRoles = ({ user }: {
user: {
image: string | null;
id: string;
name: string | null;
password: string | null;
email: string | null;
emailVerified: Date | null;
salt: string | null;
isAdmin: boolean;
isOwner: boolean;
}
}) => {
const { t } = useTranslation(['manage/users/edit', 'manage/users']);
const { data: sessionData } = useSession();
return (
<Box maw={500}>
<Title order={3}>
{t('sections.roles.title')}
</Title>
<Group mb={'md'}>
<Text>{t('sections.roles.currentRole')}</Text>
{user.isOwner ? (<Badge>{t('sections.roles.badges.owner')}</Badge>) : user.isAdmin ? (
<Badge>{t('sections.roles.badges.admin')}</Badge>) : (<Badge>{t('sections.roles.badges.normal')}</Badge>)}
</Group>
{user.isAdmin ? (
<Button
leftIcon={<IconUserDown size='1rem' />}
disabled={user.id === sessionData?.user?.id || user.isOwner}
onClick={() => {
openRoleChangeModal({
name: user.name as string,
id: user.id,
type: 'demote',
});
}}
>
{t('manage/users:tooltips.demoteAdmin')}
</Button>
) : (
<Button
leftIcon={<IconUserUp size='1rem' />}
onClick={() => {
openRoleChangeModal({
name: user.name as string,
id: user.id,
type: 'promote',
});
}}
>
{t('manage/users:tooltips.promoteToAdmin')}
</Button>
)}
</Box>
);
};

View File

@@ -0,0 +1,91 @@
import { Box, Button, Checkbox, Group, LoadingOverlay, PasswordInput, Title } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import { useInputState } from '@mantine/hooks';
import { IconAlertTriangle, IconPassword } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { z } from 'zod';
import { api } from '~/utils/api';
export const ManageUserSecurityForm = ({ userId }: { userId: string }) => {
const form = useForm({
initialValues: {
password: '',
terminateExistingSessions: false,
confirm: false,
},
validate: zodResolver(
z.object({
password: z.string().min(3),
terminateExistingSessions: z.boolean(),
confirm: z.literal(true),
})
),
validateInputOnBlur: true,
validateInputOnChange: true,
});
const [checked, setChecked] = useInputState(false);
const { t } = useTranslation(['manage/users/edit', 'common']);
const apiUtils = api.useUtils();
const { mutate, isLoading } = api.user.updatePassword.useMutation({
onSettled: () => {
void apiUtils.user.details.invalidate();
form.reset();
},
});
const handleSubmit = (values: { password: string; terminateExistingSessions: boolean }) => {
mutate({
newPassword: values.password,
terminateExistingSessions: values.terminateExistingSessions,
userId: userId,
});
setChecked(false);
};
return (
<Box maw={500}>
<LoadingOverlay visible={isLoading} />
<Title order={3}>{t('sections.security.title')}</Title>
<form onSubmit={form.onSubmit(handleSubmit)}>
<PasswordInput
icon={<IconPassword size="1rem" />}
label={t('sections.security.inputs.password.label')}
mb="md"
withAsterisk
{...form.getInputProps('password')}
/>
<Checkbox
label={t('sections.security.inputs.terminateExistingSessions.label')}
description={t('sections.security.inputs.terminateExistingSessions.description')}
mb="md"
{...form.getInputProps('terminateExistingSessions')}
/>
<Checkbox
label={t('sections.security.inputs.confirm.label')}
description={t('sections.security.inputs.confirm.description')}
checked={checked}
onClick={(event) => {
setChecked(event.currentTarget.checked);
}}
{...form.getInputProps('confirm')}
/>
<Group position="right" mt="md">
<Button
disabled={!form.isDirty() || !form.isValid()}
leftIcon={<IconAlertTriangle size="1rem" />}
loading={isLoading}
color="red"
variant="light"
type="submit"
>
{t('common:save')}
</Button>
</Group>
</form>
</Box>
);
};

View File

@@ -11,6 +11,7 @@ export const ChangeUserRoleModal = ({ id, innerProps }: ContextModalProps<InnerP
const { isLoading, mutateAsync } = api.user.changeRole.useMutation({
onSuccess: async () => {
await utils.user.all.invalidate();
await utils.user.details.invalidate();
modals.close(id);
},
});