mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 17:26:26 +01:00
✨ Add procedure for registration tokens management
This commit is contained in:
@@ -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 (
|
||||
<Stack>
|
||||
<Text>
|
||||
After the expiration, a token will no longer be valid and the recipient of the token won't
|
||||
be able to create an account.
|
||||
</Text>
|
||||
|
||||
<DateInput
|
||||
label="Expiration date"
|
||||
withAsterisk
|
||||
popoverProps={{ withinPortal: true }}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
{...form.getInputProps('expirationDate')}
|
||||
/>
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.close(id);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
expiration: form.values.expirationDate,
|
||||
});
|
||||
}}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -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' {
|
||||
|
||||
@@ -29,8 +29,6 @@ const ManageUsersPage = () => {
|
||||
}
|
||||
);
|
||||
|
||||
const { mutateAsync: deleteUserMutateAsync } = api.user.deleteUser.useMutation();
|
||||
|
||||
const [activePage, _] = useState(0);
|
||||
|
||||
return (
|
||||
@@ -39,7 +37,10 @@ const ManageUsersPage = () => {
|
||||
<title>Users • Homarr</title>
|
||||
</Head>
|
||||
|
||||
<Title mb="xl">Manage users</Title>
|
||||
<Title mb="md">Manage users</Title>
|
||||
<Text mb="xl">
|
||||
Using users, you have granular control who can access, edit or delete resources on your Homarr instance.
|
||||
</Text>
|
||||
|
||||
<Group position="apart" mb="md">
|
||||
<SegmentedControl
|
||||
@@ -74,8 +75,8 @@ const ManageUsersPage = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.pages[activePage].users.map((user) => (
|
||||
<tr>
|
||||
{data.pages[activePage].users.map((user, index) => (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<Group position="apart">
|
||||
<Group spacing="xs">
|
||||
@@ -90,8 +91,8 @@ const ManageUsersPage = () => {
|
||||
title: <Text weight="bold">Delete user ${user.name}</Text>,
|
||||
innerProps: {
|
||||
userId: user.id,
|
||||
username: user.name ?? ''
|
||||
}
|
||||
username: user.name ?? '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
color="red"
|
||||
|
||||
@@ -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 (
|
||||
<MainLayout>
|
||||
<Head>
|
||||
<title>User invites • Homarr</title>
|
||||
</Head>
|
||||
<Title>Manage user invites</Title>
|
||||
<Title mb="md">Manage user invites</Title>
|
||||
<Text mb="xl">
|
||||
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.
|
||||
</Text>
|
||||
|
||||
<Flex justify="end" mb="md">
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.openContextModal({
|
||||
modal: 'createRegistrationTokenModal',
|
||||
title: 'Create registration token',
|
||||
innerProps: {},
|
||||
});
|
||||
}}
|
||||
leftIcon={<IconPlus size="1rem" />}
|
||||
variant="default"
|
||||
>
|
||||
Create invitation
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{data && (
|
||||
<>
|
||||
<Table mb="md" withBorder highlightOnHover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Expires</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.pages[activePage].registrationTokens.map((token, index) => (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<Text>{token.id}</Text>
|
||||
</td>
|
||||
<td>
|
||||
{dayjs(dayjs()).isAfter(token.expires) ? (
|
||||
<Text>expired {dayjs(token.expires).fromNow()}</Text>
|
||||
) : (
|
||||
<Text>in {dayjs(token.expires).fromNow(true)}</Text>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<ActionIcon onClick={() => {}} color="red" variant="light">
|
||||
<IconTrash size="1rem" />
|
||||
</ActionIcon>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{data.pages[activePage].registrationTokens.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<Center p="md">
|
||||
<Text color="dimmed">There are no invitations yet.</Text>
|
||||
</Center>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
<Pagination
|
||||
total={data.pages.length}
|
||||
value={activePage + 1}
|
||||
onNextPage={fetchNextPage}
|
||||
onPreviousPage={fetchPreviousPage}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</MainLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
52
src/server/api/routers/registrationTokens.ts
Normal file
52
src/server/api/routers/registrationTokens.ts
Normal file
@@ -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'),
|
||||
}
|
||||
});
|
||||
}),
|
||||
});
|
||||
9
src/validations/registration-token.ts
Normal file
9
src/validations/registration-token.ts
Normal file
@@ -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()),
|
||||
});
|
||||
Reference in New Issue
Block a user