What is the most efficient way to validate that the user entered does not exist in the database? (Vue + vee-validate + zod)

62 views Asked by At

I have the following scheme in my vue component script to validate that the email is correct

toTypedSchema(
    z.object({
      email: z
        .string({ required_error: "El email no puede estar en blanco" })
        .email({ message: "Debe ser un email válido" })
        .refine(
          async () =>
            !(await store.dispatch("AUTH/CHECK_EMAIL_EXISTENCE", form.values.email)),
          { message: "El email ya está en uso" }
        ),
      username: z
        .string({ required_error: "El nombre de usuario no puede estar en blanco" })
        .min(4, { message: "Demasiado corto (mínimo 4 carácteres)" }),
    })
  ),
...

However, I know that it is not a good idea to put requests to the backend in vee-validate/zod validations

refine(async () => !(await store.dispatch("AUTH/CHECK_EMAIL_EXISTENCE", form.values.email)),
{ message: "El email ya está en uso" }),

because for every time the users are typed in the input a request would be made to the backend and that with many users could overload it

How do you usually approach this?

The rest of my component is like this:

<script setup lang="ts">
import { ref, watch, onMounted, computed } from "vue";

// otros
import GoogleIcon from "@/assets/svg/fill/google.svg";
import { useStore } from "vuex";
import { useToast } from "@/components/ui/toast/use-toast";

// shadcn
import { FormField, FormMessage } from "@/components/ui/form";
import Toaster from "@/components/ui/toast/Toaster.vue";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

// Componentes .vue
import Stepper from "@/components/elements/Stepper.vue";
import Loading from "@/components/elements/Loading.vue";

// zod y vee-validate
import { toTypedSchema } from "@vee-validate/zod";
import { useForm } from "vee-validate";
import * as z from "zod";

//const store = useStore();
const currentStep = ref(0);
const isLoading = ref(false);
const slideSide = ref("right");
const store = useStore();
const { toast } = useToast();

// zod
const schemas = [
  toTypedSchema(
    z.object({
      email: z
        .string({ required_error: "El email no puede estar en blanco" })
        .email({ message: "Debe ser un email válido" })
        .refine(
          async () =>
            !(await store.dispatch("AUTH/CHECK_EMAIL_EXISTENCE", form.values.email)),
          { message: "El email ya está en uso" }
        ),
      username: z
        .string({ required_error: "El nombre de usuario no puede estar en blanco" })
        .min(4, { message: "Demasiado corto (mínimo 4 carácteres)" }),
    })
  ),
  toTypedSchema(
    z.object({
      password1: z
        .string({ required_error: "El campo de la contraseña no puede estar vacío" })
        .refine((password) => /[0-9]/.test(password), {
          message: "Debe incluir al menos un número",
        })
        .refine((password) => /[a-z]/.test(password), {
          message: "Debe incluir al menos una letra minúscula",
        })
        .refine((password) => !/(qwerty)|[0-9]{4}/i.test(password), {
          message: "No puede utilizar patrones de teclado o números en secuencia",
        })
        .refine(
          (password) =>
            /^(?!^[0-9]+$)(?!^[A-Z]+$)(?!^[a-z]+$)(?!^.*([A-Za-z0-9])\1{2,}).*$/.test(
              password
            ),
          {
            message:
              "La contraseña no puede consistir únicamente en números, mayúsculas o minúsculas, ni contener caracteres repetidos",
          }
        ),
      password2: z
        .string({ required_error: "El campo de la contraseña no puede estar vacío" })
        .refine(() => form.values.password1 === form.values.password2, {
          message: "La contraseña no coincide",
        }),
    })
  ),
];

const currentSchema = computed(() => schemas[currentStep.value]);

const form = useForm({
  validationSchema: currentSchema,
  keepValuesOnUnmount: true,
}) as any;

const nextStep = async (event: Event) => {
  event.preventDefault();
  await form.validate();

  // Si no hubo errores en la validación (form.errors == {})
  if (Object.keys(form.errors.value).length === 0) currentStep.value++;
  else {
    toast({
      title: `Error en el formulario`,
      description: `Los siguientes errores fueron encontrados: ${Object.values(
        form.errors.value
      ).join(", ")}`,
    });
  }
};

const backStep = () => {
  slideSide.value = "left";
  if (currentStep.value > 0) {
    currentStep.value--;
  }
};

const registerUser = async () => {
  try {
    // Realiza la acción de registro llamando a la nueva acción en la store
    isLoading.value = true;
    await store.dispatch("AUTH/REGISTER_USER", {
      avatar: "",
      email: form.value.email,
      nickname: form.value.username,
      username: form.value.username,
      password: form.value.password2,
      pjs: [],
    });
    isLoading.value = false;
  } catch (error) {
    isLoading.value = false;
    if (error instanceof Error) {
      // Manejo de errores y muestra de Toast
      if (error.message) {
        toast({
          title: "Error en el registro",
          description: error.message,
        });
      } else {
        // Manejo de otros errores
        toast({
          title: "Error en el registro",
          description: "Ocurrió un error durante el registro.",
        });
      }
    }
  }
};

onMounted(async () => {
  currentStep.value = 0;
});

watch(
  () => form.errors.value,
  async () => {}
);

const isValid = (key: string) => !(key in form.errors.value);
</script>

<template>
  <div class="h-[350px] flex flex-col gap-5">
    <Stepper :nodes="3" :current="currentStep + 1" :direction="slideSide">
      <!-- BEGIN STEP 1 -->
      <template #step1>
        <div :class="cn('grid gap-6', $attrs.class ?? '')">
          <div class="grid gap-3">
            <div class="grid gap-1">
              <FormField v-slot="{ componentField }" name="email">
                <Input
                  label="Email"
                  id="email"
                  type="email"
                  v-bind="componentField"
                  :valid="isValid('email')"
                  :modelValue="form.values['email']"
                />
                <FormMessage />
              </FormField>

              <FormField v-slot="{ componentField }" name="username">
                <Input
                  label="Username"
                  id="username"
                  type="text"
                  v-bind="componentField"
                  :valid="isValid('username')"
                  :modelValue="form.values['username']"
                />
                <FormMessage />
              </FormField>
            </div>
            <Button :disabled="isLoading" @click="nextStep">
              <Loading v-if="isLoading" class="mr-2 h-4 w-4 animate-spin" />
              Siguiente
            </Button>
          </div>
          <!-- parte de abajo: Google, etc -->
          <div class="relative">
            <div class="absolute inset-0 flex items-center">
              <span class="w-full border-t" />
            </div>
            <div class="relative flex justify-center text-xs uppercase">
              <span class="bg-background px-2 text-muted-foreground"> o </span>
            </div>
          </div>

          <Button variant="outline" type="button" :disabled="isLoading">
            <GoogleIcon class="svgfill-foreground mr-2 h-[0.875rem] w-[0.875rem]" />
            Regístrate con Google
          </Button>
        </div>
      </template>
      <!-- END STEP 1 -->
      <!-- BEGIN STEP 2 -->
      <template #step2>
        <div class="flex flex-col gap-1">
          <FormField v-slot="{ componentField }" name="password1">
            <Input
              label="Contraseña"
              id="password1"
              type="password"
              :disabled="isLoading"
              v-bind="componentField"
              :valid="isValid('password1')"
              :modelValue="form.values['password1']"
            />
            <FormMessage />
          </FormField>

          <FormField v-slot="{ componentField }" name="password2">
            <Input
              label="Confirmar contraseña"
              id="password2"
              type="password"
              :disabled="isLoading"
              v-bind="componentField"
              :valid="isValid('password2')"
              :modelValue="form.values['password2']"
            />
            <FormMessage />
          </FormField>
        </div>
        <br />
        <div class="flex gap-x-2">
          <Button @click="backStep" variant="outline" type="button" :disabled="isLoading">
            Volver
          </Button>
          <Button @click="nextStep" variant="outline" :disabled="isLoading">
            <Loading v-if="isLoading" class="mr-2 h-4 w-4 animate-spin" />
            Siguiente
          </Button>
        </div>
      </template>
      <!-- END STEP 2 -->
      <!-- BEGIN STEP 3 -->
      <template #step3>
        <ul>
          <li><strong>Email:</strong> {{ form.values["email"] }}</li>
          <li><strong>Usuario:</strong> {{ form.values["username"] }}</li>
          <!-- No muestres las contraseñas por razones de seguridad -->
        </ul>
        <br />
        <div class="flex gap-x-2">
          <Button @click="backStep" variant="outline" type="button" :disabled="isLoading">
            Volver
          </Button>
          <Button @click="registerUser" :disabled="isLoading" type="submit">
            <Loading v-if="isLoading" class="mr-2 h-4 w-4 animate-spin" />
            Confirmar
          </Button>
        </div>
      </template>
      <!-- BEGIN STEP 3 -->
    </Stepper>
  </div>
  <div class="absolute"><Toaster /></div>
</template>

I am trying to check the existence of the email in a more efficient way

0

There are 0 answers