From 50b79cff3c690dac7c326c03d7eeb45de29c90a6 Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Tue, 18 Jun 2024 11:02:04 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20Harden=20registrati?= =?UTF-8?q?on=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(auth)/register/register-page.tsx | 190 ++++++++++-------- packages/backend/trpc/routers/auth.ts | 4 +- packages/ui/src/button.tsx | 2 +- 3 files changed, 109 insertions(+), 87 deletions(-) diff --git a/apps/web/src/app/[locale]/(auth)/register/register-page.tsx b/apps/web/src/app/[locale]/(auth)/register/register-page.tsx index 5fbee726389..1381f81a9fc 100644 --- a/apps/web/src/app/[locale]/(auth)/register/register-page.tsx +++ b/apps/web/src/app/[locale]/(auth)/register/register-page.tsx @@ -1,5 +1,14 @@ "use client"; +import { zodResolver } from "@hookform/resolvers/zod"; import { Button } from "@rallly/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@rallly/ui/form"; import { Input } from "@rallly/ui/input"; import Link from "next/link"; import { useParams, useSearchParams } from "next/navigation"; @@ -8,29 +17,32 @@ import { useTranslation } from "next-i18next"; import { usePostHog } from "posthog-js/react"; import React from "react"; import { useForm } from "react-hook-form"; +import { z } from "zod"; import { VerifyCode } from "@/components/auth/auth-forms"; import { AuthCard } from "@/components/auth/auth-layout"; import { Trans } from "@/components/trans"; import { useDayjs } from "@/utils/dayjs"; -import { requiredString, validEmail } from "@/utils/form-validation"; import { trpc } from "@/utils/trpc/client"; -type RegisterFormData = { - name: string; - email: string; -}; +const registerFormSchema = z.object({ + name: z.string().nonempty().max(100), + email: z.string().email(), +}); + +type RegisterFormData = z.infer; export const RegisterForm = () => { const { t } = useTranslation(); const { timeZone } = useDayjs(); const params = useParams<{ locale: string }>(); const searchParams = useSearchParams(); - const { register, handleSubmit, getValues, setError, formState } = - useForm({ - defaultValues: { email: "" }, - }); + const form = useForm({ + defaultValues: { email: "" }, + resolver: zodResolver(registerFormSchema), + }); + const { handleSubmit, control, getValues, setError, formState } = form; const queryClient = trpc.useUtils(); const requestRegistration = trpc.auth.requestRegistration.useMutation(); const authenticateRegistration = @@ -80,85 +92,95 @@ export const RegisterForm = () => { return (
-
{ - const res = await requestRegistration.mutateAsync({ - email: data.email, - name: data.name, - }); + + { + const res = await requestRegistration.mutateAsync({ + email: data.email, + name: data.name, + }); - if (!res.ok) { - switch (res.reason) { - case "userAlreadyExists": - setError("email", { - message: t("userAlreadyExists"), - }); - break; - case "emailNotAllowed": - setError("email", { - message: t("emailNotAllowed"), - }); + if (!res.ok) { + switch (res.reason) { + case "userAlreadyExists": + setError("email", { + message: t("userAlreadyExists"), + }); + break; + case "emailNotAllowed": + setError("email", { + message: t("emailNotAllowed"), + }); + } + } else { + setToken(res.token); } - } else { - setToken(res.token); - } - })} - > -
{t("createAnAccount")}
-

- {t("stepSummary", { - current: 1, - total: 2, })} -

-
- - - {formState.errors.name?.message ? ( -
- {formState.errors.name.message} -
- ) : null} -
-
- - - {formState.errors.email?.message ? ( -
- {formState.errors.email.message} -
- ) : null} -
- -
+
+ {t("createAnAccount")} +
+

+ {t("stepSummary", { + current: 1, + total: 2, + })} +

+
+ ( + + {t("name")} + + + + + + )} + /> + ( + + {t("email")} + + + + + + )} + /> +
+
+ +
+ +
{!getValues("email") ? (
diff --git a/packages/backend/trpc/routers/auth.ts b/packages/backend/trpc/routers/auth.ts index f987c015560..bdff2d096c1 100644 --- a/packages/backend/trpc/routers/auth.ts +++ b/packages/backend/trpc/routers/auth.ts @@ -12,8 +12,8 @@ export const auth = router({ requestRegistration: publicProcedure .input( z.object({ - name: z.string(), - email: z.string(), + name: z.string().nonempty().max(100), + email: z.string().email(), }), ) .mutation( diff --git a/packages/ui/src/button.tsx b/packages/ui/src/button.tsx index 9d64cc67220..67ba68b95cd 100644 --- a/packages/ui/src/button.tsx +++ b/packages/ui/src/button.tsx @@ -29,7 +29,7 @@ const buttonVariants = cva( size: { default: "h-9 px-2.5 gap-x-2.5 text-sm", sm: "h-7 text-sm px-1.5 gap-x-1.5 rounded-md", - lg: "h-11 text-sm gap-x-3 px-4 rounded-md", + lg: "h-11 text-base gap-x-3 px-4 rounded-md", }, }, defaultVariants: { From 13af35beaf96d7e7052f9a1ece278188be9f23ba Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Tue, 18 Jun 2024 18:48:04 +0100 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20Error=20handling=20?= =?UTF-8?q?and=20form=20hardening?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(auth)/register/register-page.tsx | 60 ++++++++++++------- .../src/components/new-participant-modal.tsx | 33 ++++++---- .../trpc/routers/polls/participants.ts | 4 +- 3 files changed, 65 insertions(+), 32 deletions(-) diff --git a/apps/web/src/app/[locale]/(auth)/register/register-page.tsx b/apps/web/src/app/[locale]/(auth)/register/register-page.tsx index 1381f81a9fc..6dff74d6552 100644 --- a/apps/web/src/app/[locale]/(auth)/register/register-page.tsx +++ b/apps/web/src/app/[locale]/(auth)/register/register-page.tsx @@ -10,6 +10,7 @@ import { FormMessage, } from "@rallly/ui/form"; import { Input } from "@rallly/ui/input"; +import { TRPCClientError } from "@trpc/client"; import Link from "next/link"; import { useParams, useSearchParams } from "next/navigation"; import { signIn } from "next-auth/react"; @@ -38,7 +39,7 @@ export const RegisterForm = () => { const params = useParams<{ locale: string }>(); const searchParams = useSearchParams(); const form = useForm({ - defaultValues: { email: "" }, + defaultValues: { email: "", name: "" }, resolver: zodResolver(registerFormSchema), }); @@ -95,25 +96,39 @@ export const RegisterForm = () => {
{ - const res = await requestRegistration.mutateAsync({ - email: data.email, - name: data.name, - }); - - if (!res.ok) { - switch (res.reason) { - case "userAlreadyExists": - setError("email", { - message: t("userAlreadyExists"), - }); - break; - case "emailNotAllowed": - setError("email", { - message: t("emailNotAllowed"), - }); + try { + await requestRegistration.mutateAsync( + { + email: data.email, + name: data.name, + }, + { + onSuccess: (res) => { + if (!res.ok) { + switch (res.reason) { + case "userAlreadyExists": + setError("email", { + message: t("userAlreadyExists"), + }); + break; + case "emailNotAllowed": + setError("email", { + message: t("emailNotAllowed"), + }); + break; + } + } else { + setToken(res.token); + } + }, + }, + ); + } catch (error) { + if (error instanceof TRPCClientError) { + setError("root", { + message: error.shape.message, + }); } - } else { - setToken(res.token); } })} > @@ -179,10 +194,15 @@ export const RegisterForm = () => { {t("continue")}
+ {formState.errors.root ? ( + + {formState.errors.root.message} + + ) : null} - {!getValues("email") ? ( + {!form.formState.isSubmitSuccessful ? (
{ const isEmailRequired = poll.requireParticipantEmail; - const { register, formState, setFocus, handleSubmit } = + const { register, setError, formState, setFocus, handleSubmit } = useForm({ resolver: zodResolver(schema), defaultValues: { @@ -102,13 +104,21 @@ export const NewParticipantForm = (props: NewParticipantModalProps) => { return (
{ - const newParticipant = await addParticipant.mutateAsync({ - name: data.name, - votes: props.votes, - email: data.email, - pollId: poll.id, - }); - props.onSubmit?.(newParticipant); + try { + const newParticipant = await addParticipant.mutateAsync({ + name: data.name, + votes: props.votes, + email: data.email, + pollId: poll.id, + }); + props.onSubmit?.(newParticipant); + } catch (error) { + if (error instanceof TRPCClientError) { + setError("root", { + message: error.shape.message, + }); + } + } })} className="space-y-4" > @@ -152,6 +162,9 @@ export const NewParticipantForm = (props: NewParticipantModalProps) => { + {formState.errors.root ? ( + {formState.errors.root.message} + ) : null}