From 01f1cebf316e62d1d62f8529448b3ecf31940027 Mon Sep 17 00:00:00 2001 From: Kenny Damgren Date: Sun, 4 Jan 2026 13:13:29 +0100 Subject: [PATCH 1/2] feat(form/auth): validate existing email and surface mapped field errors - TextField: aggregate validation messages from both meta.errors and meta.errorMap, compute isInvalid from the combined list, and pass unified allErrors to FieldError so mapped errors are shown alongside standard errors. - SignUpForm: make submit handler async and check whether the provided email already exists before mutating; if it does, set a field meta.errorMap entry ("onSubmit") for the email field to halt submit and show the specific server-side validation error. - Auth actions: add checkEmailExists server function with input validation (zod) and a db lookup to indicate if an email is already registered. Exported alongside existing auth helpers. Why: - Ensure server-side or mapped validation messages are surfaced in the UI (previously only meta.errors were shown) so users see accurate feedback. - Prevent duplicate registrations by checking email existence during sign-up and providing a clear field-level error instead of failing silently or returning a generic error. --- src/components/form/text-field.tsx | 16 ++++++++++++-- src/features/auth/api/auth-actions.ts | 13 ++++++++++++ src/features/auth/components/sign-up-form.tsx | 21 ++++++++++++++++++- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/components/form/text-field.tsx b/src/components/form/text-field.tsx index 24d8b06..9fc6297 100644 --- a/src/components/form/text-field.tsx +++ b/src/components/form/text-field.tsx @@ -23,7 +23,19 @@ export default function TextField({ icon?: React.ReactNode; }) { const field = useFieldContext(); - const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid; + + // Collect errors from both the standard errors array and errorMap + const standardErrors = field.state.meta.errors; + const errorMapErrors = Object.values(field.state.meta.errorMap).filter( + (error): error is string => typeof error === "string" + ); + const allErrors = [ + ...standardErrors, + ...errorMapErrors.map((msg) => ({ message: msg })), + ]; + + const isInvalid = allErrors.length > 0; + return ( {label} @@ -39,7 +51,7 @@ export default function TextField({ value={field.state.value as string} /> - {isInvalid && } + {isInvalid && } {description && {description}} ); diff --git a/src/features/auth/api/auth-actions.ts b/src/features/auth/api/auth-actions.ts index be1869d..4295b63 100644 --- a/src/features/auth/api/auth-actions.ts +++ b/src/features/auth/api/auth-actions.ts @@ -1,12 +1,25 @@ import { createServerFn } from "@tanstack/react-start"; import { getRequestHeaders } from "@tanstack/react-start/server"; +import { eq } from "drizzle-orm"; +import { z } from "zod"; +import { user } from "@/db/schema/auth"; import { auth } from "@/lib/server/auth"; +import { db } from "@/lib/server/db"; import { authOrRedirectMiddleware } from "@/lib/server/middleware"; export const getAuthSessionOrRedirect = createServerFn({ method: "GET" }) .middleware([authOrRedirectMiddleware]) .handler(async ({ context }) => ({ user: context.user })); +export const checkEmailExists = createServerFn({ method: "POST" }) + .inputValidator(z.object({ email: z.email() })) + .handler(async ({ data }) => { + const existingUser = await db.query.user.findFirst({ + where: eq(user.email, data.email), + }); + return { exists: !!existingUser }; + }); + export const getSession = createServerFn({ method: "GET" }).handler( async () => { const session = await auth.api.getSession({ diff --git a/src/features/auth/components/sign-up-form.tsx b/src/features/auth/components/sign-up-form.tsx index e3b914d..601bf8d 100644 --- a/src/features/auth/components/sign-up-form.tsx +++ b/src/features/auth/components/sign-up-form.tsx @@ -5,6 +5,7 @@ import { Lock, Mail, User } from "lucide-react"; import React, { useState } from "react"; import { toast } from "sonner"; import { FieldGroup } from "@/components/ui/field"; +import { checkEmailExists } from "@/features/auth/api/auth-actions"; import { authKeys } from "@/features/auth/api/auth-queries"; import { type SignUpSchema, @@ -64,11 +65,29 @@ export default function SignUpForm() { validators: { onSubmit: signUpSchema, }, - onSubmit: ({ value }) => { + onSubmit: async ({ value }) => { if (isPending) { return; } + const parsedValue = signUpSchema.safeParse(value); + const email = parsedValue.data?.email; + + if (email) { + const { exists } = await checkEmailExists({ data: { email } }); + + if (exists) { + form.setFieldMeta("email", (meta) => ({ + ...meta, + errorMap: { + ...meta.errorMap, + onSubmit: "This email is already registered", + }, + })); + return; + } + } + signUpMutate(value as SignUpSchema); }, }); From d331e6297835c1ad23ca37051e600018d3237f3d Mon Sep 17 00:00:00 2001 From: Kenny Damgren Date: Sun, 4 Jan 2026 13:20:24 +0100 Subject: [PATCH 2/2] fix(auth): simplify email extraction in sign-up form --- src/features/auth/components/sign-up-form.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/features/auth/components/sign-up-form.tsx b/src/features/auth/components/sign-up-form.tsx index 601bf8d..84212ff 100644 --- a/src/features/auth/components/sign-up-form.tsx +++ b/src/features/auth/components/sign-up-form.tsx @@ -70,8 +70,7 @@ export default function SignUpForm() { return; } - const parsedValue = signUpSchema.safeParse(value); - const email = parsedValue.data?.email; + const email = (value as SignUpSchema).email; if (email) { const { exists } = await checkEmailExists({ data: { email } });