From bd9e1334e0ca669ba7527314433883e378c3e861 Mon Sep 17 00:00:00 2001 From: Marcel Raddatz Date: Thu, 2 Apr 2026 15:02:33 +0200 Subject: [PATCH] feat(auth): add server-side validation to signup form action Validates displayName, email, password server-side before calling the backend API. Handles null from formData.get() safely. Returns structured field errors via fail(400, { errors }). Co-Authored-By: Claude Opus 4.6 --- .../routes/(public)/signup/+page.server.ts | 33 ++++++++++-- .../(public)/signup/page.server.test.ts | 51 +++++++++++++++++++ 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/frontend/src/routes/(public)/signup/+page.server.ts b/frontend/src/routes/(public)/signup/+page.server.ts index 383c855..8fa6ff5 100644 --- a/frontend/src/routes/(public)/signup/+page.server.ts +++ b/frontend/src/routes/(public)/signup/+page.server.ts @@ -5,17 +5,40 @@ import type { Actions } from './$types'; export const actions = { default: async ({ request, fetch }) => { const formData = await request.formData(); - const displayName = formData.get('displayName') as string; - const email = formData.get('email') as string; - const password = formData.get('password') as string; + const displayName = (formData.get('displayName') ?? '').toString().trim(); + const email = (formData.get('email') ?? '').toString().trim(); + const password = (formData.get('password') ?? '').toString(); + + const errors: Record = {}; + + if (!displayName) { + errors.displayName = 'Name ist erforderlich'; + } + + const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailPattern.test(email)) { + errors.email = 'Ungültige E-Mail-Adresse'; + } + + if (password.length < 8) { + errors.password = 'Mindestens 8 Zeichen'; + } + + if (Object.keys(errors).length > 0) { + return fail(400, { errors, displayName, email }); + } const api = apiClient(fetch); - const { data, error } = await api.POST('/v1/auth/signup', { + const { error } = await api.POST('/v1/auth/signup', { body: { displayName, email, password } }); if (error) { - return fail(400, { error: 'Registrierung fehlgeschlagen. Bitte versuche es erneut.' }); + return fail(400, { + errors: { form: 'Registrierung fehlgeschlagen. Bitte versuche es erneut.' }, + displayName, + email + }); } redirect(303, '/household/setup'); diff --git a/frontend/src/routes/(public)/signup/page.server.test.ts b/frontend/src/routes/(public)/signup/page.server.test.ts index 0a18c07..defc113 100644 --- a/frontend/src/routes/(public)/signup/page.server.test.ts +++ b/frontend/src/routes/(public)/signup/page.server.test.ts @@ -68,6 +68,56 @@ describe('signup form action', () => { } }); + it('rejects empty displayName with validation error', async () => { + const result = await actions.default(createRequest({ + displayName: '', + email: 'sarah@example.com', + password: 'password123' + })); + + expect(result.status).toBe(400); + expect(result.data.errors.displayName).toBe('Name ist erforderlich'); + expect(mockPost).not.toHaveBeenCalled(); + }); + + it('rejects invalid email with validation error', async () => { + const result = await actions.default(createRequest({ + displayName: 'Sarah', + email: 'notanemail', + password: 'password123' + })); + + expect(result.status).toBe(400); + expect(result.data.errors.email).toBe('Ungültige E-Mail-Adresse'); + expect(mockPost).not.toHaveBeenCalled(); + }); + + it('rejects short password with validation error', async () => { + const result = await actions.default(createRequest({ + displayName: 'Sarah', + email: 'sarah@example.com', + password: 'short' + })); + + expect(result.status).toBe(400); + expect(result.data.errors.password).toBe('Mindestens 8 Zeichen'); + expect(mockPost).not.toHaveBeenCalled(); + }); + + it('handles missing form fields without crashing', async () => { + const fd = new FormData(); + const event = { + request: { formData: () => Promise.resolve(fd) }, + fetch: vi.fn(), + cookies: { get: vi.fn(), set: vi.fn() } + } as any; + + const result = await actions.default(event); + expect(result.status).toBe(400); + expect(result.data.errors.displayName).toBe('Name ist erforderlich'); + expect(mockPost).not.toHaveBeenCalled(); + }); + it('returns fail with error message on API error', async () => { mockPost.mockResolvedValue({ data: undefined, @@ -81,5 +131,6 @@ describe('signup form action', () => { })); expect(result.status).toBe(400); + expect(result.data.errors.form).toBe('Registrierung fehlgeschlagen. Bitte versuche es erneut.'); }); });