From e273c830b4fc28f0dcf856961939935920a5d00a Mon Sep 17 00:00:00 2001 From: Manuel Date: Sat, 29 Jul 2023 17:29:57 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20create=20user=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateNewUser/create-account-step.tsx | 62 ++++++++++ .../Admin/CreateNewUser/security-step.tsx | 75 ++++++++++++ src/pages/manage/users/create.tsx | 113 +++++++++++++++--- src/pages/manage/users/index.tsx | 8 +- src/server/api/routers/user.ts | 28 ++++- src/utils/security.ts | 2 +- 6 files changed, 263 insertions(+), 25 deletions(-) create mode 100644 src/components/Admin/CreateNewUser/create-account-step.tsx create mode 100644 src/components/Admin/CreateNewUser/security-step.tsx diff --git a/src/components/Admin/CreateNewUser/create-account-step.tsx b/src/components/Admin/CreateNewUser/create-account-step.tsx new file mode 100644 index 000000000..b06531559 --- /dev/null +++ b/src/components/Admin/CreateNewUser/create-account-step.tsx @@ -0,0 +1,62 @@ +import { Button, Card, Flex, TextInput } from '@mantine/core'; +import { useForm, zodResolver } from '@mantine/form'; +import { IconArrowRight, IconAt, IconUser } from '@tabler/icons-react'; +import { z } from 'zod'; + +interface CreateAccountStepProps { + nextStep: ({ eMail, username }: { username: string; eMail: string }) => void; +} + +export const CreateAccountStep = ({ nextStep }: CreateAccountStepProps) => { + const form = useForm({ + initialValues: { + username: '', + eMail: '', + }, + validateInputOnBlur: true, + validateInputOnChange: true, + validate: zodResolver(createAccountStepValidationSchema), + }); + + return ( + + } + label="Username" + variant="filled" + mb="md" + withAsterisk + {...form.getInputProps('username')} + /> + } + label="E-Mail" + variant="filled" + mb="md" + {...form.getInputProps('eMail')} + /> + + + + + + ); +}; + +export const createAccountStepValidationSchema = z.object({ + username: z.string().min(1).max(100), + eMail: z.string().email().or(z.literal('')), +}); diff --git a/src/components/Admin/CreateNewUser/security-step.tsx b/src/components/Admin/CreateNewUser/security-step.tsx new file mode 100644 index 000000000..0ba0a9d50 --- /dev/null +++ b/src/components/Admin/CreateNewUser/security-step.tsx @@ -0,0 +1,75 @@ +import { Button, Card, Flex, Group, PasswordInput } from '@mantine/core'; +import { useForm, zodResolver } from '@mantine/form'; +import { IconArrowLeft, IconArrowRight, IconPassword } from '@tabler/icons-react'; +import { z } from 'zod'; + +interface CreateAccountSecurityStepProps { + nextStep: ({ password }: { password: string }) => void; + prevStep: () => void; +} + +export const CreateAccountSecurityStep = ({ + nextStep, + prevStep, +}: CreateAccountSecurityStepProps) => { + const form = useForm({ + initialValues: { + password: '', + }, + validateInputOnBlur: true, + validateInputOnChange: true, + validate: zodResolver(createAccountSecurityStepValidationSchema), + }); + + return ( + + + } + label="Password" + variant="filled" + mb="md" + withAsterisk + style={{ + flexGrow: 1, + }} + {...form.getInputProps('password')} + /> + + + + + + + + + ); +}; + +const randomString = () => { + return window.crypto.getRandomValues(new BigUint64Array(1))[0].toString(36); +}; + +export const createAccountSecurityStepValidationSchema = z.object({ + password: z.string().min(10).max(50), +}); diff --git a/src/pages/manage/users/create.tsx b/src/pages/manage/users/create.tsx index fda4a858a..115d4cf24 100644 --- a/src/pages/manage/users/create.tsx +++ b/src/pages/manage/users/create.tsx @@ -1,47 +1,126 @@ -import { Stepper } from '@mantine/core'; -import { IconMailCheck, IconUser } from '@tabler/icons-react'; +import { Button, Card, Flex, Group, Stepper, Text, TextInput } from '@mantine/core'; +import { useForm, zodResolver } from '@mantine/form'; +import { + IconArrowRight, + IconAt, + IconCheck, + IconLock, + IconMailCheck, + IconPremiumRights, + IconSignRight, + IconUser, +} from '@tabler/icons-react'; import Head from 'next/head'; +import Link from 'next/link'; import { useState } from 'react'; +import { z } from 'zod'; +import { + CreateAccountStep, + createAccountStepValidationSchema, +} from '~/components/Admin/CreateNewUser/create-account-step'; +import { + CreateAccountSecurityStep, + createAccountSecurityStepValidationSchema, +} from '~/components/Admin/CreateNewUser/security-step'; import { MainLayout } from '~/components/layout/admin/main-admin.layout'; +import { api } from '~/utils/api'; const CreateNewUserPage = () => { - const [active, setActive] = useState(1); + const [active, setActive] = useState(0); const nextStep = () => setActive((current) => (current < 3 ? current + 1 : current)); const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current)); + const form = useForm({ + initialValues: { + account: { + username: '', + eMail: '', + }, + security: { + password: '', + }, + }, + validate: zodResolver( + z.object({ + account: createAccountStepValidationSchema, + security: createAccountSecurityStepValidationSchema, + }) + ), + }); + + const { mutateAsync, isSuccess } = api.user.createUser.useMutation(); + return ( Create user • Homarr - + } label="First step" - description="Create an account" + description="Create account" > - Step 1 content: Create an account + { + form.setFieldValue('account', value); + nextStep(); + }} + /> + + } + label="Second step" + description="Password" + > + { + form.setFieldValue('security', value); + nextStep(); + }} + prevStep={prevStep} + /> } - label="Second step" - description="Verify email" - > - Step 2 content: Verify email - - } label="Final step" - description="Get full access" + description="Save to database" > - Step 3 content: Get full access + + + User creation has been prepared. Do you want to create the user and store it in the + database? + + + + + Completed, click back button to get to previous step diff --git a/src/pages/manage/users/index.tsx b/src/pages/manage/users/index.tsx index 38cb1976a..d60f421d2 100644 --- a/src/pages/manage/users/index.tsx +++ b/src/pages/manage/users/index.tsx @@ -28,7 +28,9 @@ const ManageUsersPage = () => { } ); - const [activePage, _] = useState(1); + const [activePage, _] = useState(0); + + console.log(data?.pages); return ( @@ -71,7 +73,7 @@ const ManageUsersPage = () => { - {data.pages[0].users.map((user) => ( + {data.pages[activePage].users.map((user) => ( @@ -92,7 +94,7 @@ const ManageUsersPage = () => { diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index a6d417bcb..02fa3b340 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -138,7 +138,7 @@ export const userRouter = createTRPCRouter({ .input( z.object({ limit: z.number().min(1).max(100).nullish(), - cursor: z.number().nullish(), + cursor: z.string().nullish(), }) ) .query(async ({ ctx, input }) => { @@ -146,13 +146,13 @@ export const userRouter = createTRPCRouter({ const cursor = input.cursor; const users = await ctx.prisma.user.findMany({ take: limit + 1, // get an extra item at the end which we'll use as next cursor - cursor: cursor ? { myCursor: cursor } : undefined, + cursor: cursor ? { id: cursor } : undefined, }); let nextCursor: typeof cursor | undefined = undefined; if (users.length > limit) { const nextItem = users.pop(); - nextCursor = nextItem!.myCursor; + nextCursor = nextItem!.id; } return { @@ -160,9 +160,29 @@ export const userRouter = createTRPCRouter({ id: user.id, name: user.name, email: user.email, - emailVerified: user.emailVerified + emailVerified: user.emailVerified, })), nextCursor, }; }), + createUser: publicProcedure + .input( + z.object({ + username: z.string(), + email: z.string().email().optional(), + password: z.string().min(8).max(100), + }) + ) + .mutation(async ({ ctx, input }) => { + const salt = bcrypt.genSaltSync(10); + const hashedPassword = hashPassword(input.password, salt); + await ctx.prisma.user.create({ + data: { + name: input.username, + email: input.email, + password: hashedPassword, + salt: salt, + }, + }); + }), }); diff --git a/src/utils/security.ts b/src/utils/security.ts index 395e9d29c..520a663c0 100644 --- a/src/utils/security.ts +++ b/src/utils/security.ts @@ -1,4 +1,4 @@ -import bcrypt from "bcrypt"; +import bcrypt from 'bcrypt'; export const hashPassword = (password: string, salt: string) => { return bcrypt.hashSync(password, salt);