--- phase: 01-foundation-auth plan: 03 type: execute wave: 2 depends_on: ["01-01", "01-02"] files_modified: - src/lib/schemas/auth.ts - src/app/actions/auth.ts - src/app/(auth)/layout.tsx - src/app/(auth)/register/page.tsx - src/app/(auth)/login/page.tsx - src/app/(auth)/verify-email/page.tsx - src/app/(auth)/reset-password/page.tsx - src/app/(auth)/update-password/page.tsx - src/app/auth/callback/route.ts - src/components/ui/button.tsx - src/components/ui/input.tsx - src/components/ui/card.tsx - src/components/auth/register-form.tsx - src/components/auth/login-form.tsx autonomous: true must_haves: truths: - "User can register with email and password" - "User receives verification email after registration" - "User cannot access app until email is verified" - "User can log in with verified email/password" - "User sees specific error messages (not generic)" - "User can reset password via email link" artifacts: - path: "src/app/actions/auth.ts" provides: "Server actions for auth operations" exports: ["registerUser", "loginUser", "resetPassword", "updatePassword"] - path: "src/app/(auth)/register/page.tsx" provides: "Registration page" min_lines: 20 - path: "src/app/(auth)/login/page.tsx" provides: "Login page" min_lines: 20 - path: "src/lib/schemas/auth.ts" provides: "Zod validation schemas" exports: ["registerSchema", "loginSchema"] key_links: - from: "src/components/auth/register-form.tsx" to: "src/app/actions/auth.ts" via: "Server Action call" pattern: "registerUser" - from: "src/app/auth/callback/route.ts" to: "Supabase Auth" via: "exchangeCodeForSession" pattern: "exchangeCodeForSession" --- Implement complete email/password authentication flow with registration, login, email verification, and password reset. Purpose: Enable users to create accounts and log in with email/password per AUTH-01 requirement. Email verification is mandatory per user decision. Output: Working auth flow where users can register, verify email, log in, and reset password. @C:\Users\miche\.claude/get-shit-done/workflows/execute-plan.md @C:\Users\miche\.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-foundation-auth/01-CONTEXT.md @.planning/phases/01-foundation-auth/01-RESEARCH.md @.planning/phases/01-foundation-auth/01-01-SUMMARY.md @.planning/phases/01-foundation-auth/01-02-SUMMARY.md Task 1: Create validation schemas and server actions src/lib/schemas/auth.ts src/app/actions/auth.ts Create validation schemas in src/lib/schemas/auth.ts: ```typescript 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 ``` Create server actions in src/app/actions/auth.ts: ```typescript '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') } ``` Password requirements per CONTEXT.md decision: - Minimum 8 characters - At least 1 number - At least 1 uppercase letter Error messages are SPECIFIC per user requirement (not generic "invalid credentials"). - Both files exist - Zod schemas validate password requirements correctly - Server actions export proper types - Error messages are in Italian and specific Validation schemas and server actions for all auth operations created. Task 2: Create auth callback route and UI components src/app/auth/callback/route.ts src/components/ui/button.tsx src/components/ui/input.tsx src/components/ui/card.tsx Create OAuth/email callback handler at src/app/auth/callback/route.ts: ```typescript import { createClient } from '@/lib/supabase/server' import { NextResponse } from 'next/server' export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url) const code = searchParams.get('code') const next = searchParams.get('next') ?? '/dashboard' if (code) { const supabase = await createClient() const { error } = await supabase.auth.exchangeCodeForSession(code) if (!error) { return NextResponse.redirect(`${origin}${next}`) } } // Return the user to an error page with instructions return NextResponse.redirect(`${origin}/login?error=auth_callback_error`) } ``` Create basic UI components. Keep them minimal but functional: src/components/ui/button.tsx: ```typescript import { forwardRef, ButtonHTMLAttributes } from 'react' import { cn } from '@/lib/utils' export interface ButtonProps extends ButtonHTMLAttributes { variant?: 'default' | 'outline' | 'ghost' size?: 'default' | 'sm' | 'lg' } const Button = forwardRef( ({ className, variant = 'default', size = 'default', ...props }, ref) => { return (

Hai gia un account?{' '} Accedi

) } ``` Create register page at src/app/(auth)/register/page.tsx: ```typescript import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { RegisterForm } from '@/components/auth/register-form' export default function RegisterPage() { return ( Crea il tuo account Inizia a usare Leopost gratuitamente ) } ``` Create login form at src/components/auth/login-form.tsx: ```typescript 'use client' import { useActionState } from 'react' import { loginUser, ActionState } from '@/app/actions/auth' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import Link from 'next/link' const initialState: ActionState = {} export function LoginForm() { const [state, formAction, pending] = useActionState(loginUser, initialState) return (
{state.error && (

{state.error}

)}
{state.fieldErrors?.email && (

{state.fieldErrors.email[0]}

)}
{state.fieldErrors?.password && (

{state.fieldErrors.password[0]}

)}
Password dimenticata?

Non hai un account?{' '} Registrati

) } ``` Create login page at src/app/(auth)/login/page.tsx: ```typescript import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { LoginForm } from '@/components/auth/login-form' export default function LoginPage({ searchParams, }: { searchParams: Promise<{ error?: string }> }) { return ( Accedi a Leopost Inserisci le tue credenziali per continuare ) } ``` Create verify-email page at src/app/(auth)/verify-email/page.tsx: ```typescript import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import Link from 'next/link' export default function VerifyEmailPage() { return ( Verifica la tua email Ti abbiamo inviato un link di conferma

Controlla la tua casella email e clicca sul link per attivare il tuo account.

Non hai ricevuto l'email? Controlla lo spam.

Torna al login
) } ``` Create reset-password page at src/app/(auth)/reset-password/page.tsx: ```typescript 'use client' import { useActionState } from 'react' import { resetPassword, ActionState } from '@/app/actions/auth' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import Link from 'next/link' const initialState: ActionState = {} export default function ResetPasswordPage() { const [state, formAction, pending] = useActionState(resetPassword, initialState) return ( Recupera password Inserisci la tua email per ricevere il link di reset {state.success ? (

{state.message}

Torna al login
) : (
{state.error && (

{state.error}

)}
{state.fieldErrors?.email && (

{state.fieldErrors.email[0]}

)}

Torna al login

)}
) } ``` Create update-password page at src/app/(auth)/update-password/page.tsx: ```typescript 'use client' import { useActionState } from 'react' import { updatePassword, ActionState } from '@/app/actions/auth' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import Link from 'next/link' const initialState: ActionState = {} export default function UpdatePasswordPage() { const [state, formAction, pending] = useActionState(updatePassword, initialState) return ( Nuova password Inserisci la tua nuova password {state.success ? (

{state.message}

Vai al login
) : (
{state.error && (

{state.error}

)}
{state.fieldErrors?.password && (

{state.fieldErrors.password[0]}

)}

Almeno 8 caratteri, 1 numero, 1 maiuscola

{state.fieldErrors?.confirmPassword && (

{state.fieldErrors.confirmPassword[0]}

)}
)}
) } ``` All text is in Italian as per project requirement. Uses React 19 useActionState (not deprecated useFormState).
- All auth pages render without errors - Forms submit using server actions - Error messages display correctly - Success states show appropriate messages - Navigation links work between pages Complete email/password auth flow with register, login, verify, and reset pages.
After all tasks complete: 1. `npm run dev` starts without errors 2. /register page allows form submission 3. /login page allows form submission 4. /reset-password page sends reset email 5. /update-password page updates password 6. Validation messages appear in Italian 7. Specific error messages show (not generic) - User can complete registration flow (form -> email) - User can log in with email/password - User sees Italian error messages - Password validation enforces medium requirements - Password reset sends email link (not code) - All forms use Server Actions (not client API calls) After completion, create `.planning/phases/01-foundation-auth/01-03-SUMMARY.md`