mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 23:45:48 +01:00
✨ Add possibility to define users as admin
This commit is contained in:
@@ -51,6 +51,7 @@ model User {
|
||||
password String?
|
||||
salt String?
|
||||
isAdmin Boolean @default(false)
|
||||
isOwner Boolean @default(false)
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
settings UserSettings?
|
||||
|
||||
@@ -14,6 +14,17 @@
|
||||
"delete": {
|
||||
"title": "Delete user {{name}}",
|
||||
"text": "Are you sure, that you want to delete the user {{name}}? This will delete data associated with this account, but not any created dashboards by this user."
|
||||
},
|
||||
"change-role": {
|
||||
"promote": {
|
||||
"title": "Promote user {{name}} to admin",
|
||||
"text": "Are you sure, that you want to promote the user {{name}} to admin? This will give the user access to all resources on your Homarr instance."
|
||||
},
|
||||
"demote": {
|
||||
"title": "Demote user {{name}} to user",
|
||||
"text": "Are you sure, that you want to demote the user {{name}} to user? This will remove the user's access to all resources on your Homarr instance."
|
||||
},
|
||||
"confirm": "Confirm"
|
||||
}
|
||||
},
|
||||
"searchDoesntMatch": "Your search does not match any entries. Please adjust your filter."
|
||||
|
||||
59
src/components/Manage/User/change-user-role.modal.tsx
Normal file
59
src/components/Manage/User/change-user-role.modal.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Button, Group, Stack, Text, Title } from '@mantine/core';
|
||||
import { ContextModalProps, modals } from '@mantine/modals';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
type InnerProps = { id: string; name: string; type: 'promote' | 'demote' };
|
||||
|
||||
export const ChangeUserRoleModal = ({ id, innerProps }: ContextModalProps<InnerProps>) => {
|
||||
const { t } = useTranslation('manage/users');
|
||||
const utils = api.useContext();
|
||||
const { isLoading, mutateAsync } = api.user.changeRole.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.user.all.invalidate();
|
||||
modals.close(id);
|
||||
},
|
||||
});
|
||||
return (
|
||||
<Stack>
|
||||
<Text>{t(`modals.change-role.${innerProps.type}.text`, innerProps)} </Text>
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.close(id);
|
||||
}}
|
||||
variant="light"
|
||||
color="gray"
|
||||
>
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await mutateAsync(innerProps);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
variant="light"
|
||||
color="red"
|
||||
>
|
||||
{t('modals.change-role.confirm')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const openRoleChangeModal = (user: InnerProps) => {
|
||||
modals.openContextModal({
|
||||
modal: 'changeUserRoleModal',
|
||||
title: (
|
||||
<Title order={4}>
|
||||
<Trans
|
||||
i18nKey={`manage/users:modals.change-role.${user.type}.title`}
|
||||
values={{ name: user.name }}
|
||||
/>
|
||||
</Title>
|
||||
),
|
||||
innerProps: user,
|
||||
});
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { DeleteBoardModal } from './components/Manage/Board/delete-board.modal';
|
||||
import { CopyInviteModal } from './components/Manage/User/Invite/copy-invite.modal';
|
||||
import { CreateInviteModal } from './components/Manage/User/Invite/create-invite.modal';
|
||||
import { DeleteInviteModal } from './components/Manage/User/Invite/delete-invite.modal';
|
||||
import { ChangeUserRoleModal } from './components/Manage/User/change-user-role.modal';
|
||||
import { DeleteUserModal } from './components/Manage/User/delete-user.modal';
|
||||
|
||||
export const modals = {
|
||||
@@ -27,6 +28,7 @@ export const modals = {
|
||||
createBoardModal: CreateBoardModal,
|
||||
copyInviteModal: CopyInviteModal,
|
||||
deleteBoardModal: DeleteBoardModal,
|
||||
changeUserRoleModal: ChangeUserRoleModal,
|
||||
};
|
||||
|
||||
declare module '@mantine/modals' {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
ActionIcon,
|
||||
Autocomplete,
|
||||
Avatar,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
@@ -12,12 +13,14 @@ import {
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
||||
import { IconPlus, IconTrash, IconUserDown, IconUserUp } from '@tabler/icons-react';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { openRoleChangeModal } from '~/components/Manage/User/change-user-role.modal';
|
||||
import { openDeleteUserModal } from '~/components/Manage/User/delete-user.modal';
|
||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
@@ -33,6 +36,7 @@ const ManageUsersPage = () => {
|
||||
page: activePage,
|
||||
search: debouncedSearch,
|
||||
});
|
||||
const { data: sessionData } = useSession();
|
||||
|
||||
const { t } = useTranslation('manage/users');
|
||||
|
||||
@@ -84,9 +88,44 @@ const ManageUsersPage = () => {
|
||||
<Group spacing="xs">
|
||||
<Avatar size="sm" />
|
||||
<Text>{user.name}</Text>
|
||||
{user.isOwner ? (
|
||||
<Badge color="pink" size="sm">
|
||||
Owner
|
||||
</Badge>
|
||||
) : user.isAdmin ? (
|
||||
<Badge color="red" size="sm">
|
||||
Admin
|
||||
</Badge>
|
||||
) : null}
|
||||
</Group>
|
||||
<Group>
|
||||
{user.isAdmin ? (
|
||||
<ActionIcon
|
||||
disabled={user.id === sessionData?.user?.id || user.isOwner}
|
||||
onClick={() => {
|
||||
openRoleChangeModal({
|
||||
...user,
|
||||
type: 'demote',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconUserDown size="1rem" />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
openRoleChangeModal({
|
||||
...user,
|
||||
type: 'promote',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconUserUp size="1rem" />
|
||||
</ActionIcon>
|
||||
)}
|
||||
|
||||
<ActionIcon
|
||||
disabled={user.id === sessionData?.user?.id || user.isOwner}
|
||||
onClick={() => {
|
||||
openDeleteUserModal(user);
|
||||
}}
|
||||
|
||||
@@ -110,7 +110,7 @@ const FirstStepContent: StepContentComponent = ({ isMobile, next }) => {
|
||||
|
||||
const SecondStepContent: StepContentComponent = ({ isMobile, next }) => {
|
||||
const [isSigninIn, setIsSigninIn] = useState(false);
|
||||
const { mutateAsync } = api.user.createAdminAccount.useMutation();
|
||||
const { mutateAsync } = api.user.createOwnerAccount.useMutation();
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
|
||||
const form = useForm<z.infer<typeof signUpFormSchema>>({
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from '../trpc';
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
createAdminAccount: publicProcedure.input(signUpFormSchema).mutation(async ({ ctx, input }) => {
|
||||
createOwnerAccount: publicProcedure.input(signUpFormSchema).mutation(async ({ ctx, input }) => {
|
||||
const userCount = await ctx.prisma.user.count();
|
||||
if (userCount > 0) {
|
||||
throw new TRPCError({
|
||||
@@ -33,7 +33,7 @@ export const userRouter = createTRPCRouter({
|
||||
colorScheme: colorSchemeParser.parse(ctx.cookies[COOKIE_COLOR_SCHEME_KEY]),
|
||||
language: ctx.cookies[COOKIE_LOCALE_KEY] ?? 'en',
|
||||
},
|
||||
isAdmin: true,
|
||||
isOwner: true,
|
||||
});
|
||||
}),
|
||||
count: publicProcedure.query(async ({ ctx }) => {
|
||||
@@ -116,6 +116,45 @@ export const userRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
}),
|
||||
changeRole: adminProcedure
|
||||
.input(z.object({ id: z.string(), type: z.enum(['promote', 'demote']) }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.session?.user?.id === input.id) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'You cannot change your own role',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await ctx.prisma.user.findUnique({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (user.isOwner) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'You cannot change the role of the owner',
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.prisma.user.update({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
data: {
|
||||
isAdmin: input.type === 'promote',
|
||||
},
|
||||
});
|
||||
}),
|
||||
changeLanguage: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
@@ -237,7 +276,8 @@ export const userRouter = createTRPCRouter({
|
||||
id: user.id,
|
||||
name: user.name!,
|
||||
email: user.email,
|
||||
emailVerified: user.emailVerified,
|
||||
isAdmin: user.isAdmin,
|
||||
isOwner: user.isOwner,
|
||||
})),
|
||||
countPages: Math.ceil(countUsers / limit),
|
||||
};
|
||||
@@ -253,6 +293,32 @@ export const userRouter = createTRPCRouter({
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const user = await ctx.prisma.user.findUnique({
|
||||
where: {
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (ctx.session?.user?.id === input.id) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'You cannot change your own role',
|
||||
});
|
||||
}
|
||||
if (user.isOwner) {
|
||||
throw new TRPCError({
|
||||
code: 'FORBIDDEN',
|
||||
message: 'You cannot change the role of the owner',
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.prisma.user.delete({
|
||||
where: {
|
||||
id: input.id,
|
||||
@@ -266,7 +332,7 @@ const createUserIfNotPresent = async (
|
||||
input: z.infer<typeof createNewUserSchema>,
|
||||
options: {
|
||||
defaultSettings?: Partial<UserSettings>;
|
||||
isAdmin?: boolean;
|
||||
isOwner?: boolean;
|
||||
} | void
|
||||
) => {
|
||||
const existingUser = await ctx.prisma.user.findFirst({
|
||||
@@ -290,7 +356,8 @@ const createUserIfNotPresent = async (
|
||||
email: input.email,
|
||||
password: hashedPassword,
|
||||
salt: salt,
|
||||
isAdmin: options?.isAdmin ?? false,
|
||||
isAdmin: options?.isOwner ?? false,
|
||||
isOwner: options?.isOwner ?? false,
|
||||
settings: {
|
||||
create: options?.defaultSettings ?? {},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user