feat(01-03): add validation schemas and server actions

- Add Zod validation schemas for auth operations
- Add server actions for register, login, reset, update password
- Add clsx and tailwind-merge for class utilities
- Password validation: 8+ chars, 1 number, 1 uppercase
- Error messages in Italian per user requirement
- Specific error messages (not generic 'invalid credentials')

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michele
2026-01-31 05:10:33 +01:00
parent bd0df408a5
commit d1156c7a03
4 changed files with 226 additions and 0 deletions

21
package-lock.json generated
View File

@@ -10,10 +10,12 @@
"dependencies": {
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.93.3",
"clsx": "^2.1.1",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-hook-form": "^7.71.1",
"tailwind-merge": "^3.4.0",
"zod": "^4.3.6"
},
"devDependencies": {
@@ -2692,6 +2694,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -6176,6 +6187,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tailwind-merge": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",

View File

@@ -11,10 +11,12 @@
"dependencies": {
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.93.3",
"clsx": "^2.1.1",
"next": "16.1.6",
"react": "19.2.3",
"react-dom": "19.2.3",
"react-hook-form": "^7.71.1",
"tailwind-merge": "^3.4.0",
"zod": "^4.3.6"
},
"devDependencies": {

164
src/app/actions/auth.ts Normal file
View File

@@ -0,0 +1,164 @@
'use server'
import { createClient } from '@/lib/supabase/server'
import { registerSchema, loginSchema, resetPasswordSchema, updatePasswordSchema } from '@/lib/schemas/auth'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
export type ActionState = {
error?: string
fieldErrors?: Record<string, string[]>
success?: boolean
message?: string
}
export async function registerUser(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const supabase = await createClient()
const parsed = registerSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
confirmPassword: formData.get('confirmPassword'),
})
if (!parsed.success) {
return {
fieldErrors: parsed.error.flatten().fieldErrors,
}
}
const { data, error } = await supabase.auth.signUp({
email: parsed.data.email,
password: parsed.data.password,
options: {
emailRedirectTo: `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback`,
}
})
if (error) {
// SPECIFIC error messages per user requirement
if (error.message.includes('already registered')) {
return { error: 'Questa email e gia registrata' }
}
if (error.message.includes('invalid')) {
return { error: 'Email non valida' }
}
return { error: error.message }
}
return {
success: true,
message: 'Registrazione completata! Controlla la tua email per confermare l\'account.'
}
}
export async function loginUser(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const supabase = await createClient()
const parsed = loginSchema.safeParse({
email: formData.get('email'),
password: formData.get('password'),
})
if (!parsed.success) {
return {
fieldErrors: parsed.error.flatten().fieldErrors,
}
}
const { data, error } = await supabase.auth.signInWithPassword({
email: parsed.data.email,
password: parsed.data.password,
})
if (error) {
// SPECIFIC error messages per user requirement
if (error.message.includes('Invalid login credentials')) {
return { error: 'Email o password errata' }
}
if (error.message.includes('Email not confirmed')) {
return { error: 'Devi confermare la tua email prima di accedere' }
}
return { error: error.message }
}
revalidatePath('/', 'layout')
redirect('/dashboard')
}
export async function resetPassword(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const supabase = await createClient()
const parsed = resetPasswordSchema.safeParse({
email: formData.get('email'),
})
if (!parsed.success) {
return {
fieldErrors: parsed.error.flatten().fieldErrors,
}
}
const { error } = await supabase.auth.resetPasswordForEmail(
parsed.data.email,
{
redirectTo: `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback?next=/update-password`,
}
)
if (error) {
return { error: error.message }
}
return {
success: true,
message: 'Se l\'email esiste, riceverai un link per reimpostare la password.'
}
}
export async function updatePassword(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const supabase = await createClient()
const parsed = updatePasswordSchema.safeParse({
password: formData.get('password'),
confirmPassword: formData.get('confirmPassword'),
})
if (!parsed.success) {
return {
fieldErrors: parsed.error.flatten().fieldErrors,
}
}
const { error } = await supabase.auth.updateUser({
password: parsed.data.password,
})
if (error) {
return { error: error.message }
}
return {
success: true,
message: 'Password aggiornata con successo!'
}
}
export async function signOut() {
const supabase = await createClient()
await supabase.auth.signOut()
revalidatePath('/', 'layout')
redirect('/login')
}

39
src/lib/schemas/auth.ts Normal file
View File

@@ -0,0 +1,39 @@
import { z } from 'zod'
export const registerSchema = z.object({
email: z.string()
.email('Email non valida'),
password: z.string()
.min(8, 'La password deve contenere almeno 8 caratteri')
.regex(/[0-9]/, 'La password deve contenere almeno un numero')
.regex(/[A-Z]/, 'La password deve contenere almeno una lettera maiuscola'),
confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
message: 'Le password non coincidono',
path: ['confirmPassword'],
})
export const loginSchema = z.object({
email: z.string().email('Email non valida'),
password: z.string().min(1, 'Password richiesta'),
})
export const resetPasswordSchema = z.object({
email: z.string().email('Email non valida'),
})
export const updatePasswordSchema = z.object({
password: z.string()
.min(8, 'La password deve contenere almeno 8 caratteri')
.regex(/[0-9]/, 'La password deve contenere almeno un numero')
.regex(/[A-Z]/, 'La password deve contenere almeno una lettera maiuscola'),
confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
message: 'Le password non coincidono',
path: ['confirmPassword'],
})
export type RegisterInput = z.infer<typeof registerSchema>
export type LoginInput = z.infer<typeof loginSchema>
export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>
export type UpdatePasswordInput = z.infer<typeof updatePasswordSchema>