Skip to content

Commit

Permalink
🔒️ Error handling and form hardening
Browse files Browse the repository at this point in the history
  • Loading branch information
lukevella committed Jun 18, 2024
1 parent 50b79cf commit 13af35b
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 32 deletions.
60 changes: 40 additions & 20 deletions apps/web/src/app/[locale]/(auth)/register/register-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -38,7 +39,7 @@ export const RegisterForm = () => {
const params = useParams<{ locale: string }>();
const searchParams = useSearchParams();
const form = useForm<RegisterFormData>({
defaultValues: { email: "" },
defaultValues: { email: "", name: "" },
resolver: zodResolver(registerFormSchema),
});

Expand Down Expand Up @@ -95,25 +96,39 @@ export const RegisterForm = () => {
<Form {...form}>
<form
onSubmit={handleSubmit(async (data) => {
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);
}
})}
>
Expand Down Expand Up @@ -179,10 +194,15 @@ export const RegisterForm = () => {
{t("continue")}
</Button>
</div>
{formState.errors.root ? (
<FormMessage className="mt-6">
{formState.errors.root.message}
</FormMessage>
) : null}
</form>
</Form>
</AuthCard>
{!getValues("email") ? (
{!form.formState.isSubmitSuccessful ? (
<div className="mt-4 pt-4 text-center text-gray-500 sm:text-base">
<Trans
i18nKey="alreadyRegistered"
Expand Down
33 changes: 23 additions & 10 deletions apps/web/src/components/new-participant-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { VoteType } from "@rallly/database";
import { Badge } from "@rallly/ui/badge";
import { Button } from "@rallly/ui/button";
import { FormMessage } from "@rallly/ui/form";
import { Input } from "@rallly/ui/input";
import { TRPCClientError } from "@trpc/client";
import clsx from "clsx";
import { useTranslation } from "next-i18next";
import { useForm } from "react-hook-form";
Expand All @@ -16,13 +18,13 @@ import VoteIcon from "./poll/vote-icon";

const requiredEmailSchema = z.object({
requireEmail: z.literal(true),
name: z.string().trim().min(1),
name: z.string().nonempty().max(100),
email: z.string().email(),
});

const optionalEmailSchema = z.object({
requireEmail: z.literal(false),
name: z.string().trim().min(1),
name: z.string().nonempty().max(100),
email: z.string().email().or(z.literal("")),
});

Expand Down Expand Up @@ -87,7 +89,7 @@ export const NewParticipantForm = (props: NewParticipantModalProps) => {

const isEmailRequired = poll.requireParticipantEmail;

const { register, formState, setFocus, handleSubmit } =
const { register, setError, formState, setFocus, handleSubmit } =
useForm<NewParticipantFormData>({
resolver: zodResolver(schema),
defaultValues: {
Expand All @@ -102,13 +104,21 @@ export const NewParticipantForm = (props: NewParticipantModalProps) => {
return (
<form
onSubmit={handleSubmit(async (data) => {
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"
>
Expand Down Expand Up @@ -152,6 +162,9 @@ export const NewParticipantForm = (props: NewParticipantModalProps) => {
<label className="mb-1 text-gray-500">{t("response")}</label>
<VoteSummary votes={props.votes} />
</fieldset>
{formState.errors.root ? (
<FormMessage>{formState.errors.root.message}</FormMessage>
) : null}
<div className="flex gap-2">
<Button onClick={props.onCancel}>{t("cancel")}</Button>
<Button
Expand Down
4 changes: 2 additions & 2 deletions packages/backend/trpc/routers/polls/participants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ export const participants = router({
.input(
z.object({
pollId: z.string(),
name: z.string().min(1, "Participant name is required"),
email: z.string().optional(),
name: z.string().min(1, "Participant name is required").max(100),
email: z.string().email().optional(),
votes: z
.object({
optionId: z.string(),
Expand Down

0 comments on commit 13af35b

Please sign in to comment.