diff --git a/packages/client/package.json b/packages/client/package.json index e2e04a09..437cb44c 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -61,11 +61,22 @@ "no-restricted-imports": [ "error", { - "name": "axios", - "importNames": [ - "default" - ], - "message": "Please use `http`, the axios client configured in the request utils (src/utils/request)." + "paths": [ + { + "name": "axios", + "importNames": [ + "default" + ], + "message": "Please use `http`, the axios client configured in the request utils (src/utils/request)." + }, + { + "name": "react-hook-form", + "importNames": [ + "useForm" + ], + "message": "Use our custom `useForm` hook instead which relies on react-hook-form (src/modules/common/hooks)." + } + ] } ], "@typescript-eslint/no-use-before-define": "off" diff --git a/packages/client/src/modules/administration/Games/Game/GameInfo.tsx b/packages/client/src/modules/administration/Games/Game/GameInfo.tsx index 3da78aae..2f2216e0 100644 --- a/packages/client/src/modules/administration/Games/Game/GameInfo.tsx +++ b/packages/client/src/modules/administration/Games/Game/GameInfo.tsx @@ -1,5 +1,5 @@ import { Box, TextField, Grid, Button, Typography } from "@mui/material"; -import { useForm, Controller } from "react-hook-form"; +import { Controller } from "react-hook-form"; import { useMutation, useQuery, useQueryClient } from "react-query"; import SaveIcon from "@mui/icons-material/Save"; import { SuccessAlert, ErrorAlert } from "../../../alert"; @@ -8,6 +8,7 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker"; import { IGame } from "../../../../utils/types"; import { http } from "../../../../utils/request"; +import { useForm } from "../../../common/hooks/useForm"; export { GameInfo }; diff --git a/packages/client/src/modules/administration/Teachers/Settings.tsx b/packages/client/src/modules/administration/Teachers/Settings.tsx index 2c97e86c..8b3f869b 100644 --- a/packages/client/src/modules/administration/Teachers/Settings.tsx +++ b/packages/client/src/modules/administration/Teachers/Settings.tsx @@ -1,7 +1,6 @@ import { Box, Button, Container, Paper, Typography } from "@mui/material"; import SaveIcon from "@mui/icons-material/Save"; import { AxiosError } from "axios"; -import { useForm } from "react-hook-form"; import { useMutation, useQueryClient } from "react-query"; import { ErrorAlert, SuccessAlert } from "../../alert"; @@ -10,6 +9,7 @@ import { User } from "../../users/types"; import { useAuth } from "../../auth/authProvider"; import { getCountryByCode } from "../../signup/components/SelectCountry"; import { http } from "../../../utils/request"; +import { useForm } from "../../common/hooks/useForm"; export { Settings }; diff --git a/packages/client/src/modules/common/hooks/useForm.formatters.ts b/packages/client/src/modules/common/hooks/useForm.formatters.ts new file mode 100644 index 00000000..255570bc --- /dev/null +++ b/packages/client/src/modules/common/hooks/useForm.formatters.ts @@ -0,0 +1,31 @@ +import { FieldValues } from "react-hook-form"; + +export { formatData }; + +const FIELD_NAME_TO_FORMATTER: Record< + string, + { + formatter: (value: string) => string; + } +> = { + email: { + formatter: formatEmail, + }, +}; + +function formatEmail(email: string = ""): string { + return email.trim().toLowerCase(); +} + +function formatData( + data: TFieldValues +): TFieldValues { + return Object.fromEntries( + Object.entries(data).map(([fieldName, fieldValue]) => { + const formatter = + FIELD_NAME_TO_FORMATTER[fieldName]?.formatter || + ((value: any) => value); + return [fieldName, formatter(fieldValue)]; + }) + ) as TFieldValues; +} diff --git a/packages/client/src/modules/common/hooks/useForm.ts b/packages/client/src/modules/common/hooks/useForm.ts new file mode 100644 index 00000000..16c368cd --- /dev/null +++ b/packages/client/src/modules/common/hooks/useForm.ts @@ -0,0 +1,45 @@ +import { useCallback, useMemo } from "react"; +import { + // eslint-disable-next-line no-restricted-imports + useForm as useFormLib, + FieldValues, + UseFormProps, + UseFormReturn, + UseFormHandleSubmit, + SubmitHandler, + SubmitErrorHandler, +} from "react-hook-form"; +import { formatData } from "./useForm.formatters"; + +export { useForm }; + +const useForm = < + TFieldValues extends FieldValues = FieldValues, + TContext = any +>( + props?: UseFormProps +): UseFormReturn => { + const form = useFormLib(props); + + const handleSubmit: UseFormHandleSubmit = useCallback( + ( + onValid: SubmitHandler, + onInvalid?: SubmitErrorHandler + ) => { + return form.handleSubmit((data: TFieldValues, ...args) => { + onValid(formatData(data), ...args); + }, onInvalid); + }, + [form] + ); + + const newForm = useMemo( + () => ({ + ...form, + handleSubmit, + }), + [form, handleSubmit] + ); + + return newForm; +}; diff --git a/packages/client/src/modules/magic-link/MagicLink.tsx b/packages/client/src/modules/magic-link/MagicLink.tsx index 1cea24a7..44a80f3d 100644 --- a/packages/client/src/modules/magic-link/MagicLink.tsx +++ b/packages/client/src/modules/magic-link/MagicLink.tsx @@ -1,4 +1,4 @@ -import { useForm } from "react-hook-form"; +import { useForm } from "../common/hooks/useForm"; import FormInput from "../common/components/FormInput"; import { Link } from "react-router-dom"; import { sendMagicLink } from "../users/services"; diff --git a/packages/client/src/modules/play/MyGames/MyGames.tsx b/packages/client/src/modules/play/MyGames/MyGames.tsx index 76f9d662..f5eb7295 100644 --- a/packages/client/src/modules/play/MyGames/MyGames.tsx +++ b/packages/client/src/modules/play/MyGames/MyGames.tsx @@ -1,6 +1,6 @@ import { trim } from "lodash"; import { Box, CircularProgress, Grid, TextField } from "@mui/material"; -import { Controller, useForm } from "react-hook-form"; +import { Controller } from "react-hook-form"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { ErrorAlert } from "../../alert"; import { PlayBox } from "../Components"; @@ -17,6 +17,7 @@ import { useTranslation } from "../../translations/useTranslation"; import { Typography } from "../../common/components/Typography"; import { useAlerts } from "../../alert/AlertProvider"; import { Button } from "../../common/components/Button"; +import { useForm } from "../../common/hooks/useForm"; export { MyGames }; diff --git a/packages/client/src/modules/play/Personalization/PersonalizationForm.tsx b/packages/client/src/modules/play/Personalization/PersonalizationForm.tsx index 41704958..a7f2089a 100644 --- a/packages/client/src/modules/play/Personalization/PersonalizationForm.tsx +++ b/packages/client/src/modules/play/Personalization/PersonalizationForm.tsx @@ -3,7 +3,7 @@ import { Button, Grid, Tooltip, Typography } from "@mui/material"; import { CustomContainer } from "./styles/personalization"; import { BackArrow, BackArrowWithValidation } from "./common/BackArrow"; import { QuestionLine, QuestionText } from "./styles/form"; -import { useForm } from "react-hook-form"; +import { useForm } from "../../common/hooks/useForm"; import { PersoFormInputList, PersoFormNumberInput } from "./common/FormInputs"; import { formSections, diff --git a/packages/client/src/modules/signup/components/Form/Form.tsx b/packages/client/src/modules/signup/components/Form/Form.tsx index 95ebf02e..9f5b4d07 100644 --- a/packages/client/src/modules/signup/components/Form/Form.tsx +++ b/packages/client/src/modules/signup/components/Form/Form.tsx @@ -1,5 +1,5 @@ import FormInput from "../../../common/components/FormInput"; -import { useForm } from "react-hook-form"; +import { useForm } from "../../../common/hooks/useForm"; import CheckboxWithText from "../CheckboxWithText"; import { NewUser } from "../../../users/services"; import { useMutation } from "react-query"; diff --git a/packages/server/src/modules/users/controllers/index.ts b/packages/server/src/modules/users/controllers/index.ts index 786dcf80..1ae9af52 100644 --- a/packages/server/src/modules/users/controllers/index.ts +++ b/packages/server/src/modules/users/controllers/index.ts @@ -7,6 +7,7 @@ import { rolesServices } from "../../roles/services"; import { services } from "../services"; import { signInController } from "./signInController"; import { getManyUsersController } from "./getManyUsersController"; +import { getEmailSchema } from "../../utils/schemaParser"; const crudController = { getDocumentController, @@ -27,7 +28,7 @@ export { controllers }; async function signUpController(request: Request, response: Response) { const bodySchema = z.object({ country: z.string(), - email: z.string(), + email: getEmailSchema(), lastName: z.string(), firstName: z.string(), }); @@ -57,7 +58,10 @@ async function getLoggedUserController(request: Request, response: Response) { } async function sendMagicLinkController(request: Request, response: Response) { - const { email } = request.body; + const bodySchema = z.object({ + email: getEmailSchema(), + }); + const { email } = bodySchema.parse(request.body); await services.sendMagicLink(email); response .status(200) @@ -76,7 +80,7 @@ async function getTeamForPlayer(request: Request, response: Response) { const bodySchemaUpdate = z.object({ country: z.string().optional(), - email: z.string().optional(), + email: getEmailSchema(), lastName: z.string().optional(), firstName: z.string().optional(), roleId: z.number().optional(), diff --git a/packages/server/src/modules/utils/schemaParser.ts b/packages/server/src/modules/utils/schemaParser.ts new file mode 100644 index 00000000..938fed85 --- /dev/null +++ b/packages/server/src/modules/utils/schemaParser.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export { getEmailSchema }; + +const getEmailSchema = () => + z + .string() + .email() + .transform((email) => (email || "").trim().toLowerCase());