feat: add validation schemas for login and signup, integrate validation in respective components

This commit is contained in:
juan 2025-08-10 18:30:07 +02:00
parent 51c7b9f86d
commit e45772e2a9
9 changed files with 150 additions and 27 deletions

54
package-lock.json generated
View File

@ -22,7 +22,8 @@
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4",
"zod": "^4.0.17"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.1.3", "@biomejs/biome": "2.1.3",
@ -3663,6 +3664,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@netlify/zip-it-and-ship-it/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -6632,6 +6642,15 @@
"vite": ">=6.0.0" "vite": ">=6.0.0"
} }
}, },
"node_modules/@tanstack/react-start-plugin/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@tanstack/react-start-server": { "node_modules/@tanstack/react-start-server": {
"version": "1.130.12", "version": "1.130.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-start-server/-/react-start-server-1.130.12.tgz", "resolved": "https://registry.npmjs.org/@tanstack/react-start-server/-/react-start-server-1.130.12.tgz",
@ -6767,6 +6786,15 @@
"url": "https://github.com/sponsors/tannerlinsley" "url": "https://github.com/sponsors/tannerlinsley"
} }
}, },
"node_modules/@tanstack/router-generator/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@tanstack/router-plugin": { "node_modules/@tanstack/router-plugin": {
"version": "1.130.15", "version": "1.130.15",
"resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.130.15.tgz", "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.130.15.tgz",
@ -6820,6 +6848,15 @@
} }
} }
}, },
"node_modules/@tanstack/router-plugin/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@tanstack/router-utils": { "node_modules/@tanstack/router-utils": {
"version": "1.130.12", "version": "1.130.12",
"resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.130.12.tgz", "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.130.12.tgz",
@ -6938,6 +6975,15 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@tanstack/start-plugin-core/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/@tanstack/start-server-core": { "node_modules/@tanstack/start-server-core": {
"version": "1.130.12", "version": "1.130.12",
"resolved": "https://registry.npmjs.org/@tanstack/start-server-core/-/start-server-core-1.130.12.tgz", "resolved": "https://registry.npmjs.org/@tanstack/start-server-core/-/start-server-core-1.130.12.tgz",
@ -14740,9 +14786,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.25.76", "version": "4.0.17",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

View File

@ -28,7 +28,8 @@
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4",
"zod": "^4.0.17"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.1.3", "@biomejs/biome": "2.1.3",

View File

@ -0,0 +1,37 @@
import { useState } from "react"
import type { z } from "zod"
type FormDataValidation = Record<string, FormDataEntryValue>
export const useValidation = <T,>({
defaultSchema
}: {
defaultSchema?: z.ZodSchema<T>
}) => {
const [errors, setErrors] = useState<T>()
const validate = ({
formData,
schema
}: {
formData: FormDataValidation
schema?: z.ZodSchema<T>
}) => {
const result =
schema?.safeParse(formData) ?? defaultSchema?.safeParse(formData)
if (!result) {
throw new Error("No schema provided")
}
if (!result.success) {
setErrors(result.error.flatten().fieldErrors as T)
return false
}
setErrors(undefined)
return true
}
return { errors, validate }
}

View File

@ -3,11 +3,12 @@ import { getRouteApi } from "@tanstack/react-router"
import { createServerFn, useServerFn } from "@tanstack/react-start" import { createServerFn, useServerFn } from "@tanstack/react-start"
import { toast } from "sonner" import { toast } from "sonner"
import { getSupabaseServerClient } from "@/integrations/supabase/supabase" import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
import { loginSchema } from "../schemas/login"
const apiRouter = getRouteApi("/login") const apiRouter = getRouteApi("/login")
export const loginFn = createServerFn({ method: "POST" }) export const loginFn = createServerFn({ method: "POST" })
.validator((d: { email: string; password: string }) => d) .validator(loginSchema)
.handler(async ({ data }) => { .handler(async ({ data }) => {
const supabase = getSupabaseServerClient() const supabase = getSupabaseServerClient()
const response = await supabase.auth.signInWithPassword({ const response = await supabase.auth.signInWithPassword({
@ -36,12 +37,9 @@ export const mutationLogin = () => {
} }
}) })
}, },
onSuccess: (data, ctx) => { onSuccess: (data) => {
console.log("Login successful", data)
console.log("ctx", ctx)
if (data?.error) { if (data?.error) {
toast.error(data.message) toast.error(data.message, { id: "login" })
return return
} }
toast.success("Login successful! Redirecting to posts...", { toast.success("Login successful! Redirecting to posts...", {

View File

@ -3,12 +3,11 @@ import { getRouteApi, redirect } from "@tanstack/react-router"
import { createServerFn, useServerFn } from "@tanstack/react-start" import { createServerFn, useServerFn } from "@tanstack/react-start"
import { toast } from "sonner" import { toast } from "sonner"
import { getSupabaseServerClient } from "@/integrations/supabase/supabase" import { getSupabaseServerClient } from "@/integrations/supabase/supabase"
import { signupSchema } from "../schemas/signup"
const apiRouter = getRouteApi("/signup") const apiRouter = getRouteApi("/signup")
export const signupFn = createServerFn({ method: "POST" }) export const signupFn = createServerFn({ method: "POST" })
.validator( .validator(signupSchema)
(d: { email: string; password: string; redirectUrl?: string }) => d
)
.handler(async ({ data }) => { .handler(async ({ data }) => {
const supabase = getSupabaseServerClient() const supabase = getSupabaseServerClient()
const { error } = await supabase.auth.signUp({ const { error } = await supabase.auth.signUp({
@ -47,7 +46,9 @@ export const mutationSignup = () => {
}) })
}, },
onSuccess: () => { onSuccess: () => {
toast.success("Signup successful! Redirecting to login...", { id: "signup" }) toast.success("Signup successful! Redirecting to login...", {
id: "signup"
})
navigate({ navigate({
to: "/login" to: "/login"
}) })

6
src/lib/schemas/login.ts Normal file
View File

@ -0,0 +1,6 @@
import z from "zod"
export const loginSchema = z.object({
email: z.email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters long")
})

View File

@ -0,0 +1,7 @@
import z from "zod"
export const signupSchema = z.object({
email: z.email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters long"),
redirectUrl: z.string().optional()
})

View File

@ -1,28 +1,44 @@
import { Button, Form, Input } from "@heroui/react" import { Button, Form, Input } from "@heroui/react"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { useValidation } from "@/lib/hooks/useValidation"
import { mutationLogin } from "@/lib/mutations/mutationLogin" import { mutationLogin } from "@/lib/mutations/mutationLogin"
import { loginSchema } from "@/lib/schemas/login"
export const Route = createFileRoute("/login")({ export const Route = createFileRoute("/login")({
component: LoginComp component: LoginComp
}) })
function LoginComp() { function LoginComp() {
const loginMutation = mutationLogin(); const loginMutation = mutationLogin()
const { errors, validate } = useValidation({
defaultSchema: loginSchema
})
return ( return (
<div className="flex justify-center items-center flex-col h-screen"> <div className="flex justify-center items-center flex-col h-screen">
<p className="font-semibold mb-3">Login</p> <p className="font-semibold mb-3">Login</p>
<Form <Form
validationErrors={errors}
className="grid gap-2 max-w-sm w-full" className="grid gap-2 max-w-sm w-full"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault() e.preventDefault()
const formData = new FormData(e.currentTarget) const formData = new FormData(e.currentTarget)
const email = formData.get("email") as string const email = formData.get("email") as string
const password = formData.get("password") as string const password = formData.get("password") as string
loginMutation.mutate({ email, password }) if (
validate({
formData: { email, password }
})
)
loginMutation.mutate({ email, password })
}} }}
> >
<Input name="email" type="email" placeholder="Email" /> <Input name="email" label="Email" />
<Input name="password" type="password" placeholder="Password" /> <Input name="password" type="password" label="Password" />
<Button type="submit">Enviar</Button> <Button type="submit" isLoading={loginMutation.isPending}>
Entrar
</Button>
</Form> </Form>
</div> </div>
) )

View File

@ -1,6 +1,8 @@
import { Button, Form, Input } from "@heroui/react" import { Button, Form, Input } from "@heroui/react"
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { useValidation } from "@/lib/hooks/useValidation"
import { mutationSignup } from "@/lib/mutations/mutationSignup" import { mutationSignup } from "@/lib/mutations/mutationSignup"
import { signupSchema } from "@/lib/schemas/signup"
export const Route = createFileRoute("/signup")({ export const Route = createFileRoute("/signup")({
component: SignupComp component: SignupComp
@ -8,24 +10,33 @@ export const Route = createFileRoute("/signup")({
function SignupComp() { function SignupComp() {
const signupMutation = mutationSignup() const signupMutation = mutationSignup()
const { errors, validate } = useValidation({
defaultSchema: signupSchema
})
return ( return (
<div className="flex justify-center items-center flex-col h-screen"> <div className="flex justify-center items-center flex-col h-screen">
<p className="font-semibold mb-3">Signup</p> <p className="font-semibold mb-3">Signup</p>
<Form <Form
className="grid gap-2 max-w-5xl w-full" validationErrors={errors}
className="grid gap-2 max-w-sm w-full"
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault() e.preventDefault()
const formData = new FormData(e.currentTarget) const formData = new FormData(e.currentTarget)
const email = formData.get("email") as string const email = formData.get("email") as string
const password = formData.get("password") as string const password = formData.get("password") as string
signupMutation.mutate({ if (
email, validate({
password formData: { email, password }
}) })
)
signupMutation.mutate({
email,
password
})
}} }}
> >
<Input name="email" type="email" placeholder="Email" /> <Input name="email" type="email" label="Email" />
<Input name="password" type="password" placeholder="Password" /> <Input name="password" type="password" label="Password" />
<Button type="submit" isLoading={signupMutation.isPending}> <Button type="submit" isLoading={signupMutation.isPending}>
Enviar Enviar
</Button> </Button>