♻️ Rename registration token to invite, add created by

This commit is contained in:
Meier Lukas
2023-08-01 11:43:24 +02:00
parent df890b8c0a
commit f93d935175
18 changed files with 109 additions and 97 deletions

View File

@@ -53,6 +53,7 @@ model User {
accounts Account[] accounts Account[]
sessions Session[] sessions Session[]
settings UserSettings? settings UserSettings?
createdInvites Invite[]
} }
model VerificationToken { model VerificationToken {
@@ -63,10 +64,12 @@ model VerificationToken {
@@unique([identifier, token]) @@unique([identifier, token])
} }
model RegistrationToken { model Invite {
id String @id @default(cuid()) id String @id @default(cuid())
token String @unique token String @unique
expires DateTime expires DateTime
createdById String
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
} }
model UserSettings { model UserSettings {

View File

@@ -14,7 +14,7 @@
} }
}, },
"buttons": { "buttons": {
"submit": "Register" "submit": "Create account"
} }
} }
} }

View File

@@ -23,7 +23,7 @@ export const Search = ({ isMobile }: SearchProps) => {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const ref = useRef<HTMLInputElement>(null); const ref = useRef<HTMLInputElement>(null);
useHotkeys([['mod+K', () => ref.current?.focus()]]); useHotkeys([['mod+K', () => ref.current?.focus()]]);
const { data: userWithSettings } = api.user.getWithSettings.useQuery(); const { data: userWithSettings } = api.user.withSettings.useQuery();
const { config } = useConfigContext(); const { config } = useConfigContext();
const { colors } = useMantineTheme(); const { colors } = useMantineTheme();
const router = useRouter(); const router = useRouter();

View File

@@ -2,7 +2,7 @@ import { Button, Mark, Stack, Text } from '@mantine/core';
import { ContextModalProps, modals } from '@mantine/modals'; import { ContextModalProps, modals } from '@mantine/modals';
import Link from 'next/link'; import Link from 'next/link';
export const CopyRegistrationToken = ({ export const CopyInviteModal = ({
context, context,
id, id,
innerProps, innerProps,

View File

@@ -5,24 +5,20 @@ import { ContextModalProps, modals } from '@mantine/modals';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver'; import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
import { createRegistrationTokenSchema } from '~/validations/registration-token'; import { createInviteSchema } from '~/validations/invite';
export const CreateRegistrationTokenModal = ({ export const CreateInviteModal = ({ id }: ContextModalProps<{}>) => {
context,
id,
innerProps,
}: ContextModalProps<{}>) => {
const apiContext = api.useContext(); const apiContext = api.useContext();
const { isLoading, mutateAsync } = api.registrationTokens.createRegistrationToken.useMutation({ const { isLoading, mutateAsync } = api.invites.create.useMutation({
onSuccess: async (data) => { onSuccess: async (data) => {
await apiContext.registrationTokens.getAllInvites.invalidate(); await apiContext.invites.all.invalidate();
modals.close(id); modals.close(id);
modals.openContextModal({ modals.openContextModal({
modal: 'copyRegistrationTokenModal', modal: 'copyInviteModal',
title: <Text weight="bold">Copy invitation</Text>, title: <Text weight="bold">Copy invitation</Text>,
innerProps: data, innerProps: data,
}) });
}, },
}); });
@@ -35,14 +31,14 @@ export const CreateRegistrationTokenModal = ({
initialValues: { initialValues: {
expirationDate: dayjs().add(7, 'days').toDate(), expirationDate: dayjs().add(7, 'days').toDate(),
}, },
validate: i18nZodResolver(createRegistrationTokenSchema), validate: i18nZodResolver(createInviteSchema),
}); });
return ( return (
<Stack> <Stack>
<Text> <Text>
After the expiration, a token will no longer be valid and the recipient of the token won't After the expiration, an invite will no longer be valid and the recipient of the invite
be able to create an account. won't be able to create an account.
</Text> </Text>
<DateInput <DateInput

View File

@@ -2,15 +2,15 @@ import { Button, Group, Stack, Text } from '@mantine/core';
import { ContextModalProps, modals } from '@mantine/modals'; import { ContextModalProps, modals } from '@mantine/modals';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
export const DeleteRegistrationTokenModal = ({ export const DeleteInviteModal = ({
context, context,
id, id,
innerProps, innerProps,
}: ContextModalProps<{ tokenId: string }>) => { }: ContextModalProps<{ tokenId: string }>) => {
const apiContext = api.useContext(); const apiContext = api.useContext();
const { isLoading, mutateAsync } = api.registrationTokens.deleteRegistrationToken.useMutation({ const { isLoading, mutateAsync } = api.invites.delete.useMutation({
onSuccess: async () => { onSuccess: async () => {
await apiContext.registrationTokens.getAllInvites.invalidate(); await apiContext.invites.all.invalidate();
modals.close(id); modals.close(id);
}, },
}); });
@@ -18,7 +18,7 @@ export const DeleteRegistrationTokenModal = ({
<Stack> <Stack>
<Text> <Text>
Are you sure, that you want to delete this invitation? Users with this link will no longer Are you sure, that you want to delete this invitation? Users with this link will no longer
be able to register using that link. be able to create an account using that link.
</Text> </Text>
<Group grow> <Group grow>

View File

@@ -10,7 +10,7 @@ export const DeleteUserModal = ({
const apiContext = api.useContext(); const apiContext = api.useContext();
const { isLoading, mutateAsync } = api.user.deleteUser.useMutation({ const { isLoading, mutateAsync } = api.user.deleteUser.useMutation({
onSuccess: async () => { onSuccess: async () => {
await apiContext.user.getAll.invalidate(); await apiContext.user.all.invalidate();
modals.close(id); modals.close(id);
}, },
}); });

View File

@@ -6,12 +6,12 @@ import { WidgetsEditModal } from '~/components/Dashboard/Tiles/Widgets/WidgetsEd
import { WidgetsRemoveModal } from '~/components/Dashboard/Tiles/Widgets/WidgetsRemoveModal'; import { WidgetsRemoveModal } from '~/components/Dashboard/Tiles/Widgets/WidgetsRemoveModal';
import { CategoryEditModal } from '~/components/Dashboard/Wrappers/Category/CategoryEditModal'; import { CategoryEditModal } from '~/components/Dashboard/Wrappers/Category/CategoryEditModal';
import { DeleteUserModal } from './delete-user/delete-user.modal'; import { CopyInviteModal } from './copy-invite/copy-invite.modal';
import { CreateRegistrationTokenModal } from './create-registration-token/create-registration-token.modal';
import { DeleteRegistrationTokenModal } from './delete-registration-token/delete-registration-token.modal';
import { CreateDashboardModal } from './create-dashboard/create-dashboard.modal'; import { CreateDashboardModal } from './create-dashboard/create-dashboard.modal';
import { CopyRegistrationToken } from './copy-regristration-token/copy-registration-token.modal'; import { CreateInviteModal } from './create-invite/create-invite.modal';
import { DeleteBoardModal } from './delete-board/delete-board.modal'; import { DeleteBoardModal } from './delete-board/delete-board.modal';
import { DeleteInviteModal } from './delete-invite/delete-invite.modal';
import { DeleteUserModal } from './delete-user/delete-user.modal';
export const modals = { export const modals = {
editApp: EditAppModal, editApp: EditAppModal,
@@ -22,11 +22,11 @@ export const modals = {
changeAppPositionModal: ChangeAppPositionModal, changeAppPositionModal: ChangeAppPositionModal,
changeIntegrationPositionModal: ChangeWidgetPositionModal, changeIntegrationPositionModal: ChangeWidgetPositionModal,
deleteUserModal: DeleteUserModal, deleteUserModal: DeleteUserModal,
createRegistrationTokenModal: CreateRegistrationTokenModal, createInviteModal: CreateInviteModal,
deleteRegistrationTokenModal: DeleteRegistrationTokenModal, deleteInviteModal: DeleteInviteModal,
createDashboardModal: CreateDashboardModal, createDashboardModal: CreateDashboardModal,
copyRegistrationTokenModal: CopyRegistrationToken, copyInviteModal: CopyInviteModal,
deleteBoardModal: DeleteBoardModal deleteBoardModal: DeleteBoardModal,
}; };
declare module '@mantine/modals' { declare module '@mantine/modals' {

View File

@@ -8,17 +8,17 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { z } from 'zod'; import { z } from 'zod';
import { prisma } from '~/server/db'; import { prisma } from '~/server/db';
import { registerNamespaces } from '~/tools/server/translation-namespaces'; import { inviteNamespaces } from '~/tools/server/translation-namespaces';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver'; import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
import { signUpFormSchema } from '~/validations/user'; import { signUpFormSchema } from '~/validations/user';
export default function AuthInvitePage() { export default function AuthInvitePage() {
const { t } = useTranslation('authentication/register'); const { t } = useTranslation('authentication/invite');
const { i18nZodResolver } = useI18nZodResolver(); const { i18nZodResolver } = useI18nZodResolver();
const router = useRouter(); const router = useRouter();
const query = router.query as { token: string }; const query = router.query as { token: string };
const { mutateAsync } = api.user.register.useMutation(); const { mutateAsync } = api.user.createFromInvite.useMutation();
const form = useForm<z.infer<typeof signUpFormSchema>>({ const form = useForm<z.infer<typeof signUpFormSchema>>({
validateInputOnChange: true, validateInputOnChange: true,
@@ -37,7 +37,7 @@ export default function AuthInvitePage() {
void mutateAsync( void mutateAsync(
{ {
...values, ...values,
registerToken: query.token, inviteToken: query.token,
}, },
{ {
onSuccess() { onSuccess() {
@@ -124,7 +124,7 @@ export const getServerSideProps: GetServerSideProps = async ({ locale, query, pa
}; };
} }
const token = await prisma.registrationToken.findUnique({ const token = await prisma.invite.findUnique({
where: { where: {
id: routeParams.data.inviteId, id: routeParams.data.inviteId,
token: queryParams.data.token, token: queryParams.data.token,
@@ -139,7 +139,7 @@ export const getServerSideProps: GetServerSideProps = async ({ locale, query, pa
return { return {
props: { props: {
...(await serverSideTranslations(locale ?? '', registerNamespaces)), ...(await serverSideTranslations(locale ?? '', inviteNamespaces)),
}, },
}; };
}; };

View File

@@ -49,9 +49,9 @@ const CreateNewUserPage = () => {
}); });
const context = api.useContext(); const context = api.useContext();
const { mutateAsync, isLoading } = api.user.createUser.useMutation({ const { mutateAsync, isLoading } = api.user.create.useMutation({
onSettled: () => { onSettled: () => {
void context.user.getAll.invalidate(); void context.user.all.invalidate();
}, },
onSuccess: () => { onSuccess: () => {
nextStep(); nextStep();

View File

@@ -24,7 +24,7 @@ const ManageUsersPage = () => {
const [activePage, setActivePage] = useState(0); const [activePage, setActivePage] = useState(0);
const [nonDebouncedSearch, setNonDebouncedSearch] = useState<string | undefined>(''); const [nonDebouncedSearch, setNonDebouncedSearch] = useState<string | undefined>('');
const [debouncedSearch] = useDebouncedValue<string | undefined>(nonDebouncedSearch, 200); const [debouncedSearch] = useDebouncedValue<string | undefined>(nonDebouncedSearch, 200);
const { data } = api.user.getAll.useQuery({ const { data } = api.user.all.useQuery({
page: activePage, page: activePage,
search: debouncedSearch, search: debouncedSearch,
}); });

View File

@@ -19,7 +19,7 @@ import { api } from '~/utils/api';
const ManageUserInvitesPage = () => { const ManageUserInvitesPage = () => {
const [activePage, setActivePage] = useState(0); const [activePage, setActivePage] = useState(0);
const { data } = api.registrationTokens.getAllInvites.useQuery({ const { data } = api.invites.all.useQuery({
page: activePage, page: activePage,
}); });
@@ -40,17 +40,17 @@ const ManageUserInvitesPage = () => {
</Head> </Head>
<Title mb="md">Manage user invites</Title> <Title mb="md">Manage user invites</Title>
<Text mb="xl"> <Text mb="xl">
Using registration tokens, you can invite users to your Homarr instance. An invitation will Using invites, you can invite users to your Homarr instance. An invitation will only be
only be valid for a certain time-span and can be used once. The expiration must be between 5 valid for a certain time-span and can be used once. The expiration must be between 5 minutes
minutes and 12 months upon creation. and 12 months upon creation.
</Text> </Text>
<Flex justify="end" mb="md"> <Flex justify="end" mb="md">
<Button <Button
onClick={() => { onClick={() => {
modals.openContextModal({ modals.openContextModal({
modal: 'createRegistrationTokenModal', modal: 'createInviteModal',
title: 'Create registration token', title: 'Create invite',
innerProps: {}, innerProps: {},
}); });
}} }}
@@ -67,31 +67,35 @@ const ManageUserInvitesPage = () => {
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Creator</th>
<th>Expires</th> <th>Expires</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.registrationTokens.map((token, index) => ( {data.invites.map((invite, index) => (
<tr key={index}> <tr key={index}>
<td className={classes.tableIdCell}> <td className={classes.tableGrowCell}>
<Text lineClamp={1}>{token.id}</Text> <Text lineClamp={1}>{invite.id}</Text>
</td>
<td className={classes.tableGrowCell}>
<Text lineClamp={1}>{invite.creator}</Text>
</td> </td>
<td className={classes.tableCell}> <td className={classes.tableCell}>
{dayjs(dayjs()).isAfter(token.expires) ? ( {dayjs(dayjs()).isAfter(invite.expires) ? (
<Text>expired {dayjs(token.expires).fromNow()}</Text> <Text>expired {dayjs(invite.expires).fromNow()}</Text>
) : ( ) : (
<Text>in {dayjs(token.expires).fromNow(true)}</Text> <Text>in {dayjs(invite.expires).fromNow(true)}</Text>
)} )}
</td> </td>
<td className={classes.tableCell}> <td className={classes.tableCell}>
<ActionIcon <ActionIcon
onClick={() => { onClick={() => {
modals.openContextModal({ modals.openContextModal({
modal: 'deleteRegistrationTokenModal', modal: 'deleteInviteModal',
title: <Text weight="bold">Delete registration token</Text>, title: <Text weight="bold">Delete invite</Text>,
innerProps: { innerProps: {
tokenId: token.id, tokenId: invite.id,
}, },
}); });
}} }}
@@ -103,7 +107,7 @@ const ManageUserInvitesPage = () => {
</td> </td>
</tr> </tr>
))} ))}
{data.registrationTokens.length === 0 && ( {data.invites.length === 0 && (
<tr> <tr>
<td colSpan={3}> <td colSpan={3}>
<Center p="md"> <Center p="md">
@@ -137,8 +141,8 @@ const ManageUserInvitesPage = () => {
}; };
const useStyles = createStyles(() => ({ const useStyles = createStyles(() => ({
tableIdCell: { tableGrowCell: {
width: '100%', width: '50%',
}, },
tableCell: { tableCell: {
whiteSpace: 'nowrap', whiteSpace: 'nowrap',

View File

@@ -14,7 +14,7 @@ import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
import { updateSettingsValidationSchema } from '~/validations/user'; import { updateSettingsValidationSchema } from '~/validations/user';
const PreferencesPage = ({ locale }: InferGetServerSidePropsType<typeof getServerSideProps>) => { const PreferencesPage = ({ locale }: InferGetServerSidePropsType<typeof getServerSideProps>) => {
const { data } = api.user.getWithSettings.useQuery(); const { data } = api.user.withSettings.useQuery();
return ( return (
<ManageLayout> <ManageLayout>
@@ -34,7 +34,7 @@ export const [FormProvider, useFormContext, useForm] =
const SettingsComponent = ({ const SettingsComponent = ({
settings, settings,
}: { }: {
settings: RouterOutputs['user']['getWithSettings']['settings']; settings: RouterOutputs['user']['withSettings']['settings'];
}) => { }) => {
const languagesData = languages.map((language) => ({ const languagesData = languages.map((language) => ({
image: 'https://img.icons8.com/clouds/256/000000/futurama-bender.png', image: 'https://img.icons8.com/clouds/256/000000/futurama-bender.png',

View File

@@ -8,6 +8,7 @@ import { dnsHoleRouter } from './routers/dns-hole';
import { dockerRouter } from './routers/docker/router'; import { dockerRouter } from './routers/docker/router';
import { downloadRouter } from './routers/download'; import { downloadRouter } from './routers/download';
import { iconRouter } from './routers/icon'; import { iconRouter } from './routers/icon';
import { inviteRouter } from './routers/invite';
import { mediaRequestsRouter } from './routers/media-request'; import { mediaRequestsRouter } from './routers/media-request';
import { mediaServerRouter } from './routers/media-server'; import { mediaServerRouter } from './routers/media-server';
import { overseerrRouter } from './routers/overseerr'; import { overseerrRouter } from './routers/overseerr';
@@ -15,7 +16,6 @@ import { rssRouter } from './routers/rss';
import { usenetRouter } from './routers/usenet/router'; import { usenetRouter } from './routers/usenet/router';
import { userRouter } from './routers/user'; import { userRouter } from './routers/user';
import { weatherRouter } from './routers/weather'; import { weatherRouter } from './routers/weather';
import { inviteRouter } from './routers/registrationTokens';
/** /**
* This is the primary router for your server. * This is the primary router for your server.
@@ -38,7 +38,7 @@ export const rootRouter = createTRPCRouter({
usenet: usenetRouter, usenet: usenetRouter,
calendar: calendarRouter, calendar: calendarRouter,
weather: weatherRouter, weather: weatherRouter,
registrationTokens: inviteRouter invites: inviteRouter,
}); });
// export type definition of API // export type definition of API

View File

@@ -2,34 +2,42 @@ import { randomBytes } from 'crypto';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { z } from 'zod'; import { z } from 'zod';
import { createTRPCRouter, publicProcedure } from '../trpc'; import { adminProcedure, createTRPCRouter, publicProcedure } from '../trpc';
export const inviteRouter = createTRPCRouter({ export const inviteRouter = createTRPCRouter({
getAllInvites: publicProcedure all: adminProcedure
.input( .input(
z.object({ z.object({
limit: z.number().min(1).max(100).nullish().default(10), limit: z.number().min(1).max(100).nullish().default(10),
page: z.number().min(0) page: z.number().min(0),
}) })
) )
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const limit = input.limit ?? 50; const limit = input.limit ?? 50;
const registrationTokens = await ctx.prisma.registrationToken.findMany({ const invites = await ctx.prisma.invite.findMany({
take: limit, take: limit,
skip: limit * input.page, skip: limit * input.page,
include: {
createdBy: {
select: {
name: true,
},
},
},
}); });
const countRegistrationTokens = await ctx.prisma.registrationToken.count(); const inviteCount = await ctx.prisma.invite.count();
return { return {
registrationTokens: registrationTokens.map((token) => ({ invites: invites.map((token) => ({
id: token.id, id: token.id,
expires: token.expires, expires: token.expires,
creator: token.createdBy.name,
})), })),
countPages: Math.ceil(countRegistrationTokens / limit) countPages: Math.ceil(inviteCount / limit),
}; };
}), }),
createRegistrationToken: publicProcedure create: adminProcedure
.input( .input(
z.object({ z.object({
expiration: z expiration: z
@@ -39,9 +47,10 @@ export const inviteRouter = createTRPCRouter({
}) })
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const token = await ctx.prisma.registrationToken.create({ const token = await ctx.prisma.invite.create({
data: { data: {
expires: input.expiration, expires: input.expiration,
createdById: ctx.session.user.id,
token: randomBytes(20).toString('hex'), token: randomBytes(20).toString('hex'),
}, },
}); });
@@ -52,10 +61,10 @@ export const inviteRouter = createTRPCRouter({
expires: token.expires, expires: token.expires,
}; };
}), }),
deleteRegistrationToken: publicProcedure delete: adminProcedure
.input(z.object({ tokenId: z.string() })) .input(z.object({ tokenId: z.string() }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await ctx.prisma.registrationToken.delete({ await ctx.prisma.invite.delete({
where: { where: {
id: input.tokenId, id: input.tokenId,
}, },

View File

@@ -13,25 +13,25 @@ import { COOKIE_COLOR_SCHEME_KEY, COOKIE_LOCALE_KEY } from '../../../../data/con
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'; import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
export const userRouter = createTRPCRouter({ export const userRouter = createTRPCRouter({
register: publicProcedure createFromInvite: publicProcedure
.input( .input(
signUpFormSchema.and( signUpFormSchema.and(
z.object({ z.object({
registerToken: z.string(), inviteToken: z.string(),
}) })
) )
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const token = await ctx.prisma.registrationToken.findUnique({ const token = await ctx.prisma.invite.findUnique({
where: { where: {
token: input.registerToken, token: input.inviteToken,
}, },
}); });
if (!token || token.expires < new Date()) { if (!token || token.expires < new Date()) {
throw new TRPCError({ throw new TRPCError({
code: 'FORBIDDEN', code: 'FORBIDDEN',
message: 'Invalid registration token', message: 'Invalid invite token',
}); });
} }
@@ -64,7 +64,7 @@ export const userRouter = createTRPCRouter({
}, },
}, },
}); });
await ctx.prisma.registrationToken.delete({ await ctx.prisma.invite.delete({
where: { where: {
id: token.id, id: token.id,
}, },
@@ -115,7 +115,7 @@ export const userRouter = createTRPCRouter({
}, },
}); });
}), }),
getWithSettings: protectedProcedure.query(async ({ ctx, input }) => { withSettings: protectedProcedure.query(async ({ ctx, input }) => {
const user = await ctx.prisma.user.findUnique({ const user = await ctx.prisma.user.findUnique({
where: { where: {
id: ctx.session?.user?.id, id: ctx.session?.user?.id,
@@ -158,7 +158,7 @@ export const userRouter = createTRPCRouter({
}); });
}), }),
getAll: publicProcedure all: publicProcedure
.input( .input(
z.object({ z.object({
limit: z.number().min(1).max(100).default(10), limit: z.number().min(1).max(100).default(10),
@@ -199,7 +199,7 @@ export const userRouter = createTRPCRouter({
countPages: Math.ceil(countUsers / limit), countPages: Math.ceil(countUsers / limit),
}; };
}), }),
createUser: publicProcedure.input(createNewUserSchema).mutation(async ({ ctx, input }) => { create: publicProcedure.input(createNewUserSchema).mutation(async ({ ctx, input }) => {
const salt = bcrypt.genSaltSync(10); const salt = bcrypt.genSaltSync(10);
const hashedPassword = hashPassword(input.password, salt); const hashedPassword = hashPassword(input.password, salt);
await ctx.prisma.user.create({ await ctx.prisma.user.create({

View File

@@ -51,4 +51,4 @@ export const dashboardNamespaces = [
export const loginNamespaces = ['authentication/login', 'zod']; export const loginNamespaces = ['authentication/login', 'zod'];
export const registerNamespaces = ['authentication/register', 'zod']; export const inviteNamespaces = ['authentication/invite', 'zod'];

View File

@@ -1,7 +1,7 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { z } from 'zod'; import { z } from 'zod';
export const createRegistrationTokenSchema = z.object({ export const createInviteSchema = z.object({
expiration: z expiration: z
.date() .date()
.min(dayjs().add(5, 'minutes').toDate()) .min(dayjs().add(5, 'minutes').toDate())