Skip to content

Commit

Permalink
Added client-side form checks with Zod before server submit
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianLeChat committed Jan 17, 2024
1 parent 62e6b51 commit 1f11807
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 52 deletions.
54 changes: 36 additions & 18 deletions app/[locale]/components/authentication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
"use client";

import Link from "next/link";
import { z } from "zod";
import schema from "@/schemas/authentication";
import { merge } from "@/utilities/tailwind";
import { useForm } from "react-hook-form";
import serverAction from "@/utilities/recaptcha";
import { zodResolver } from "@hookform/resolvers/zod";
import { useFormState } from "react-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGithub, faGoogle } from "@fortawesome/free-brands-svg-icons";
Expand Down Expand Up @@ -52,7 +54,8 @@ export default function Authentification()
const [ passwordType, setPasswordType ] = useState( "password" );

// Déclaration du formulaire.
const form = useForm( {
const form = useForm<z.infer<typeof schema>>( {
resolver: zodResolver( schema ),
defaultValues: {
email: "",
password: "",
Expand Down Expand Up @@ -137,8 +140,22 @@ export default function Authentification()

<Form {...form}>
<form
action={( formData ) => serverAction( signUpAction, formData )}
onSubmit={() => setLoading( true )}
action={async ( formData: FormData ) =>
{
// Vérifications côté client.
const state = await form.trigger();

if ( !state )
{
return false;
}

// Activation de l'état de chargement.
setLoading( true );

// Exécution de l'action côté serveur.
return serverAction( signUpAction, formData );
}}
className="space-y-6"
>
{/* Adresse électronique */}
Expand All @@ -156,10 +173,6 @@ export default function Authentification()
{...field}
type="email"
disabled={loading}
minLength={
schema.shape.email
.minLength as number
}
maxLength={
schema.shape.email
.maxLength as number
Expand Down Expand Up @@ -233,10 +246,6 @@ export default function Authentification()
{...field}
type="email"
disabled={loading}
minLength={
schema.shape.email
.minLength as number
}
maxLength={
schema.shape.email
.maxLength as number
Expand Down Expand Up @@ -285,11 +294,6 @@ export default function Authentification()
? "opacity-25"
: ""
}`}
minLength={
schema.shape.password
._def.options[ 0 ]
.minLength as number
}
maxLength={
schema.shape.password
._def.options[ 0 ]
Expand Down Expand Up @@ -424,8 +428,22 @@ export default function Authentification()

{/* Fournisseurs d'authentification externes */}
<form
action={( formData ) => serverAction( signInAction, formData )}
onSubmit={() => setLoading( true )}
action={async ( formData: FormData ) =>
{
// Vérifications côté client.
const state = await form.trigger();

if ( !state )
{
return false;
}

// Activation de l'état de chargement.
setLoading( true );

// Exécution de l'action côté serveur.
return serverAction( signInAction, formData );
}}
>
<input type="hidden" name="provider" value="google" />

Expand Down
2 changes: 1 addition & 1 deletion app/[locale]/components/ui/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ const FormMessage = forwardRef<
>( ( { className, children, ...props }, ref ) =>
{
const { error, formMessageId } = useFormField();
const body = error ? String( error?.message ) : children;
const body = error ? String( error?.type ) : children;

if ( !body )
{
Expand Down
30 changes: 27 additions & 3 deletions app/[locale]/dashboard/components/file-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

"use client";

import { z } from "zod";
import schema from "@/schemas/file-upload";
import { merge } from "@/utilities/tailwind";
import { useForm } from "react-hook-form";
import { useEffect, useState } from "react";
import serverAction from "@/utilities/recaptcha";
import { useSession } from "next-auth/react";
import { formatSize } from "@/utilities/react-table";
import { zodResolver } from "@hookform/resolvers/zod";
import { useFormState } from "react-dom";
import { type FileAttributes } from "@/interfaces/File";
import type { Table, TableMeta } from "@tanstack/react-table";
Expand Down Expand Up @@ -44,6 +47,12 @@ export default function FileUpload( {
// Déclaration des constantes.
const states = table.options.meta as TableMeta<FileAttributes>;
const maxQuota = Number( process.env.NEXT_PUBLIC_MAX_QUOTA ?? 0 );
const fileSchema = schema.omit( { upload: true } ).extend( {
// Modification de la vérification du fichier pour prendre en compte
// la différence entre les données côté client et celles envoyées
// côté serveur.
upload: z.string()
} );

// Déclaration des variables d'état.
const session = useSession();
Expand All @@ -58,7 +67,8 @@ export default function FileUpload( {

// Déclaration du formulaire.
const percent = Number( ( ( states.quota / maxQuota ) * 100 ).toFixed( 2 ) );
const form = useForm( {
const form = useForm<z.infer<typeof fileSchema>>( {
resolver: zodResolver( fileSchema ),
defaultValues: {
upload: ""
}
Expand Down Expand Up @@ -213,8 +223,22 @@ export default function FileUpload( {

<Form {...form}>
<form
action={( formData ) => serverAction( uploadAction, formData )}
onSubmit={() => states.setLoading( [ "modal" ] )}
action={async ( formData: FormData ) =>
{
// Vérifications côté client.
const state = await form.trigger();

if ( !state )
{
return false;
}

// Activation de l'état de chargement.
states.setLoading( [ "modal" ] );

// Exécution de l'action côté serveur.
return serverAction( uploadAction, formData );
}}
className="space-y-6"
>
<FormField
Expand Down
1 change: 0 additions & 1 deletion app/[locale]/dashboard/components/share-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,6 @@ export default function ShareManager()
value={search}
onChange={( event ) => setSearch( event.target.value )}
disabled={isLoading}
minLength={1}
maxLength={50}
className="mt-3"
spellCheck="false"
Expand Down
34 changes: 21 additions & 13 deletions app/[locale]/settings/components/account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@

"use client";

import * as z from "zod";
import schema from "@/schemas/account";
import { useForm } from "react-hook-form";
import serverAction from "@/utilities/recaptcha";
import { useLocale } from "next-intl";
import { zodResolver } from "@hookform/resolvers/zod";
import { useFormState } from "react-dom";
import type { Session } from "next-auth";
import { useState, useEffect } from "react";
Expand Down Expand Up @@ -56,10 +58,11 @@ export default function Account( { session }: { session: Session } )
const [ passwordType, setPasswordType ] = useState( "text" );

// Déclaration du formulaire.
const form = useForm( {
const form = useForm<z.infer<typeof schema>>( {
resolver: zodResolver( schema ),
defaultValues: {
username: session.user.name ?? "",
language: useLocale(),
language: useLocale() as "en" | "fr",
password: ""
}
} );
Expand Down Expand Up @@ -134,8 +137,22 @@ export default function Account( { session }: { session: Session } )
return (
<Form {...form}>
<form
action={( formData ) => serverAction( updateAction, formData )}
onSubmit={() => setLoading( true )}
action={async ( formData: FormData ) =>
{
// Vérifications côté client.
const state = await form.trigger();

if ( !state )
{
return false;
}

// Activation de l'état de chargement.
setLoading( true );

// Exécution de l'action côté serveur.
return serverAction( updateAction, formData );
}}
className="space-y-8"
>
{/* Nom d'affichage */}
Expand All @@ -152,10 +169,6 @@ export default function Account( { session }: { session: Session } )
<FormControl>
<Input
{...field}
minLength={
schema.shape.username
.minLength as number
}
maxLength={
schema.shape.username
.maxLength as number
Expand Down Expand Up @@ -250,11 +263,6 @@ export default function Account( { session }: { session: Session } )
{...field}
type={passwordType}
onKeyDown={() => setPasswordType( "password" )}
minLength={
schema.shape.password._def
.options[ 0 ]
.minLength as number
}
maxLength={
schema.shape.password._def
.options[ 0 ]
Expand Down
30 changes: 20 additions & 10 deletions app/[locale]/settings/components/issue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

"use client";

import * as z from "zod";
import schema from "@/schemas/issue";
import { List,
Send,
Expand All @@ -13,6 +14,7 @@ import { List,
ShieldAlert } from "lucide-react";
import { useForm } from "react-hook-form";
import serverAction from "@/utilities/recaptcha";
import { zodResolver } from "@hookform/resolvers/zod";
import { useFormState } from "react-dom";
import { useState, useEffect } from "react";

Expand Down Expand Up @@ -45,7 +47,8 @@ export default function Account()
} );

// Déclaration du formulaire.
const form = useForm( {
const form = useForm<z.infer<typeof schema>>( {
resolver: zodResolver( schema ),
defaultValues: {
area: "account",
subject: "",
Expand Down Expand Up @@ -106,8 +109,22 @@ export default function Account()
return (
<Form {...form}>
<form
action={( formData ) => serverAction( updateAction, formData )}
onSubmit={() => setLoading( true )}
action={async ( formData: FormData ) =>
{
// Vérifications côté client.
const state = await form.trigger();

if ( !state )
{
return false;
}

// Activation de l'état de chargement.
setLoading( true );

// Exécution de l'action côté serveur.
return serverAction( updateAction, formData );
}}
className="space-y-8"
>
<div className="flex gap-4 max-sm:flex-col">
Expand Down Expand Up @@ -241,9 +258,6 @@ export default function Account()
{...field}
id="subject"
disabled={loading}
minLength={
schema.shape.subject.minLength as number
}
maxLength={
schema.shape.subject.maxLength as number
}
Expand Down Expand Up @@ -276,10 +290,6 @@ export default function Account()
{...field}
id="description"
disabled={loading}
minLength={
schema.shape.description
.minLength as number
}
maxLength={
schema.shape.description
.maxLength as number
Expand Down
Loading

0 comments on commit 1f11807

Please sign in to comment.