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