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:
21
package-lock.json
generated
21
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
164
src/app/actions/auth.ts
Normal 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
39
src/lib/schemas/auth.ts
Normal 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>
|
||||
Reference in New Issue
Block a user