From b4c188e797acaff1ca7f92b7363d3ff990ccfe82 Mon Sep 17 00:00:00 2001 From: Manuel Date: Sat, 29 Jul 2023 21:11:52 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20procedure=20for=20registratio?= =?UTF-8?q?n=20tokens=20management?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../create-registration-token.modal.tsx | 72 +++++++++++++++ src/modals/modals.ts | 2 + src/pages/manage/users/index.tsx | 15 ++-- src/pages/manage/users/invites.tsx | 90 ++++++++++++++++++- src/server/api/root.ts | 2 + src/server/api/routers/registrationTokens.ts | 52 +++++++++++ src/validations/registration-token.ts | 9 ++ 7 files changed, 232 insertions(+), 10 deletions(-) create mode 100644 src/modals/create-registration-token/create-registration-token.modal.tsx create mode 100644 src/server/api/routers/registrationTokens.ts create mode 100644 src/validations/registration-token.ts diff --git a/src/modals/create-registration-token/create-registration-token.modal.tsx b/src/modals/create-registration-token/create-registration-token.modal.tsx new file mode 100644 index 000000000..ffc65a6cf --- /dev/null +++ b/src/modals/create-registration-token/create-registration-token.modal.tsx @@ -0,0 +1,72 @@ +import { Button, Group, Stack, Text } from '@mantine/core'; +import { DateInput } from '@mantine/dates'; +import { useForm, zodResolver } from '@mantine/form'; +import { ContextModalProps, modals } from '@mantine/modals'; +import dayjs from 'dayjs'; +import { api } from '~/utils/api'; +import { useI18nZodResolver } from '~/utils/i18n-zod-resolver'; +import { createRegistrationTokenSchema } from '~/validations/registration-token'; + +export const CreateRegistrationTokenModal = ({ + context, + id, + innerProps, +}: ContextModalProps<{}>) => { + const apiContext = api.useContext(); + const { isLoading, mutateAsync } = api.registrationTokens.createRegistrationToken.useMutation({ + onSuccess: async () => { + await apiContext.registrationTokens.getAllInvites.invalidate(); + modals.close(id); + }, + }); + + const { i18nZodResolver } = useI18nZodResolver(); + + const minDate = dayjs().add(5, 'minutes').toDate(); + const maxDate = dayjs().add(6, 'months').toDate(); + + const form = useForm({ + initialValues: { + expirationDate: dayjs().add(7, 'days').toDate(), + }, + validate: i18nZodResolver(createRegistrationTokenSchema), + }); + + return ( + + + After the expiration, a token will no longer be valid and the recipient of the token won't + be able to create an account. + + + + + + + + + + ); +}; diff --git a/src/modals/modals.ts b/src/modals/modals.ts index c5ace5220..f58eba345 100644 --- a/src/modals/modals.ts +++ b/src/modals/modals.ts @@ -7,6 +7,7 @@ import { WidgetsRemoveModal } from '~/components/Dashboard/Tiles/Widgets/Widgets import { CategoryEditModal } from '~/components/Dashboard/Wrappers/Category/CategoryEditModal'; import { DeleteUserModal } from './delete-user/delete-user.modal'; +import { CreateRegistrationTokenModal } from './create-registration-token/create-registration-token.modal'; export const modals = { editApp: EditAppModal, @@ -17,6 +18,7 @@ export const modals = { changeAppPositionModal: ChangeAppPositionModal, changeIntegrationPositionModal: ChangeWidgetPositionModal, deleteUserModal: DeleteUserModal, + createRegistrationTokenModal: CreateRegistrationTokenModal }; declare module '@mantine/modals' { diff --git a/src/pages/manage/users/index.tsx b/src/pages/manage/users/index.tsx index f4f4c0207..4646bd9e8 100644 --- a/src/pages/manage/users/index.tsx +++ b/src/pages/manage/users/index.tsx @@ -29,8 +29,6 @@ const ManageUsersPage = () => { } ); - const { mutateAsync: deleteUserMutateAsync } = api.user.deleteUser.useMutation(); - const [activePage, _] = useState(0); return ( @@ -39,7 +37,10 @@ const ManageUsersPage = () => { Users • Homarr - Manage users + Manage users + + Using users, you have granular control who can access, edit or delete resources on your Homarr instance. + { - {data.pages[activePage].users.map((user) => ( - + {data.pages[activePage].users.map((user, index) => ( + @@ -90,8 +91,8 @@ const ManageUsersPage = () => { title: Delete user ${user.name}, innerProps: { userId: user.id, - username: user.name ?? '' - } + username: user.name ?? '', + }, }); }} color="red" diff --git a/src/pages/manage/users/invites.tsx b/src/pages/manage/users/invites.tsx index 8c0762a23..e88a037d8 100644 --- a/src/pages/manage/users/invites.tsx +++ b/src/pages/manage/users/invites.tsx @@ -1,14 +1,98 @@ -import { Title } from '@mantine/core'; -import { Head } from 'next/document'; +import { ActionIcon, Button, Center, Flex, Pagination, Table, Text, Title } from '@mantine/core'; +import { modals } from '@mantine/modals'; +import { IconPlus, IconTrash } from '@tabler/icons-react'; +import dayjs from 'dayjs'; +import Head from 'next/head'; +import { useState } from 'react'; import { MainLayout } from '~/components/layout/admin/main-admin.layout'; +import { modals as applicationModals } from '~/modals/modals'; +import { api } from '~/utils/api'; const ManageUserInvitesPage = () => { + const { data, isFetched, fetchPreviousPage, fetchNextPage } = + api.registrationTokens.getAllInvites.useInfiniteQuery({ + limit: 10, + }); + + const [activePage, _] = useState(0); + return ( User invites • Homarr - Manage user invites + Manage user invites + + Using registration tokens, you can invite users to your Homarr instance. An invitation will + only be valid for a certain time-span and can be used once. The expiration must be between 5 + minutes and 12 months upon creation. + + + + + + + {data && ( + <> + + + + + + + + + + {data.pages[activePage].registrationTokens.map((token, index) => ( + + + + + + ))} + {data.pages[activePage].registrationTokens.length === 0 && ( + + + + )} + +
IDExpiresActions
+ {token.id} + + {dayjs(dayjs()).isAfter(token.expires) ? ( + expired {dayjs(token.expires).fromNow()} + ) : ( + in {dayjs(token.expires).fromNow(true)} + )} + + {}} color="red" variant="light"> + + +
+
+ There are no invitations yet. +
+
+ + + )}
); }; diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 8f261c685..850554714 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -15,6 +15,7 @@ import { rssRouter } from './routers/rss'; import { usenetRouter } from './routers/usenet/router'; import { userRouter } from './routers/user'; import { weatherRouter } from './routers/weather'; +import { inviteRouter } from './routers/registrationTokens'; /** * This is the primary router for your server. @@ -37,6 +38,7 @@ export const rootRouter = createTRPCRouter({ usenet: usenetRouter, calendar: calendarRouter, weather: weatherRouter, + registrationTokens: inviteRouter }); // export type definition of API diff --git a/src/server/api/routers/registrationTokens.ts b/src/server/api/routers/registrationTokens.ts new file mode 100644 index 000000000..5ce1c345d --- /dev/null +++ b/src/server/api/routers/registrationTokens.ts @@ -0,0 +1,52 @@ +import dayjs from 'dayjs'; +import { z } from 'zod'; + +import { createTRPCRouter, publicProcedure } from '../trpc'; +import { randomBytes } from 'crypto'; + +export const inviteRouter = createTRPCRouter({ + getAllInvites: publicProcedure + .input( + z.object({ + limit: z.number().min(1).max(100).nullish(), + cursor: z.string().nullish(), + }) + ) + .query(async ({ ctx, input }) => { + const limit = input.limit ?? 50; + const cursor = input.cursor; + const registrationTokens = await ctx.prisma.registrationToken.findMany({ + take: limit + 1, // get an extra item at the end which we'll use as next cursor + cursor: cursor ? { id: cursor } : undefined, + }); + + let nextCursor: typeof cursor | undefined = undefined; + if (registrationTokens.length > limit) { + const nextItem = registrationTokens.pop(); + nextCursor = nextItem!.id; + } + + return { + registrationTokens: registrationTokens.map((token) => ({ + id: token.id, + expires: token.expires, + })), + nextCursor, + }; + }), + createRegistrationToken: publicProcedure.input( + z.object({ + expiration: z + .date() + .min(dayjs().add(5, 'minutes').toDate()) + .max(dayjs().add(6, 'months').toDate()), + }) + ).mutation(async ({ ctx, input }) => { + await ctx.prisma.registrationToken.create({ + data: { + expires: input.expiration, + token: randomBytes(20).toString('hex'), + } + }); + }), +}); diff --git a/src/validations/registration-token.ts b/src/validations/registration-token.ts new file mode 100644 index 000000000..2964ba471 --- /dev/null +++ b/src/validations/registration-token.ts @@ -0,0 +1,9 @@ +import dayjs from 'dayjs'; +import { z } from 'zod'; + +export const createRegistrationTokenSchema = z.object({ + expiration: z + .date() + .min(dayjs().add(5, 'minutes').toDate()) + .max(dayjs().add(6, 'months').toDate()), +});