diff --git a/package-lock.json b/package-lock.json index 59c9081..d844574 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 132f25c..5431dfe 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/app/actions/auth.ts b/src/app/actions/auth.ts new file mode 100644 index 0000000..ab82f69 --- /dev/null +++ b/src/app/actions/auth.ts @@ -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 + success?: boolean + message?: string +} + +export async function registerUser( + prevState: ActionState, + formData: FormData +): Promise { + 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 { + 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 { + 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 { + 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') +} diff --git a/src/lib/schemas/auth.ts b/src/lib/schemas/auth.ts new file mode 100644 index 0000000..a56cf9a --- /dev/null +++ b/src/lib/schemas/auth.ts @@ -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 +export type LoginInput = z.infer +export type ResetPasswordInput = z.infer +export type UpdatePasswordInput = z.infer