Files
Michele bd3e1074a8 docs(01): create phase 1 plans - Foundation & Auth
Phase 01: Foundation & Auth
- 6 plans in 4 execution waves
- Wave 1: Project setup (01) + Database schema (02) [parallel]
- Wave 2: Email/password auth (03) + Google OAuth (04) [parallel]
- Wave 3: Middleware & route protection (05)
- Wave 4: Subscription management UI (06)

Requirements covered: AUTH-01, AUTH-02, AUTH-03
Ready for execution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 03:12:38 +01:00

1030 lines
32 KiB
Markdown

---
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"
---
<objective>
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.
</objective>
<execution_context>
@C:\Users\miche\.claude/get-shit-done/workflows/execute-plan.md
@C:\Users\miche\.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
</context>
<tasks>
<task type="auto">
<name>Task 1: Create validation schemas and server actions</name>
<files>
src/lib/schemas/auth.ts
src/app/actions/auth.ts
</files>
<action>
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<typeof registerSchema>
export type LoginInput = z.infer<typeof loginSchema>
export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>
export type UpdatePasswordInput = z.infer<typeof updatePasswordSchema>
```
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<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')
}
```
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").
</action>
<verify>
- Both files exist
- Zod schemas validate password requirements correctly
- Server actions export proper types
- Error messages are in Italian and specific
</verify>
<done>
Validation schemas and server actions for all auth operations created.
</done>
</task>
<task type="auto">
<name>Task 2: Create auth callback route and UI components</name>
<files>
src/app/auth/callback/route.ts
src/components/ui/button.tsx
src/components/ui/input.tsx
src/components/ui/card.tsx
</files>
<action>
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<HTMLButtonElement> {
variant?: 'default' | 'outline' | 'ghost'
size?: 'default' | 'sm' | 'lg'
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant = 'default', size = 'default', ...props }, ref) => {
return (
<button
className={cn(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
{
'bg-blue-600 text-white hover:bg-blue-700': variant === 'default',
'border border-gray-300 bg-white hover:bg-gray-50': variant === 'outline',
'hover:bg-gray-100': variant === 'ghost',
},
{
'h-10 px-4 py-2': size === 'default',
'h-8 px-3 text-sm': size === 'sm',
'h-12 px-6 text-lg': size === 'lg',
},
className
)}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = 'Button'
export { Button }
```
src/components/ui/input.tsx:
```typescript
import { forwardRef, InputHTMLAttributes } from 'react'
import { cn } from '@/lib/utils'
export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
error?: boolean
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, error, ...props }, ref) => {
return (
<input
className={cn(
'flex h-10 w-full rounded-md border bg-white px-3 py-2 text-sm',
'placeholder:text-gray-400',
'focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1',
'disabled:cursor-not-allowed disabled:opacity-50',
error ? 'border-red-500' : 'border-gray-300',
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export { Input }
```
src/components/ui/card.tsx:
```typescript
import { HTMLAttributes, forwardRef } from 'react'
import { cn } from '@/lib/utils'
const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-lg border border-gray-200 bg-white shadow-sm',
className
)}
{...props}
/>
)
)
Card.displayName = 'Card'
const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
/>
)
)
CardHeader.displayName = 'CardHeader'
const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
{...props}
/>
)
)
CardTitle.displayName = 'CardTitle'
const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => (
<p
ref={ref}
className={cn('text-sm text-gray-500', className)}
{...props}
/>
)
)
CardDescription.displayName = 'CardDescription'
const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
)
)
CardContent.displayName = 'CardContent'
const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
)
)
CardFooter.displayName = 'CardFooter'
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
```
Create utility function src/lib/utils.ts:
```typescript
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
```
Install clsx and tailwind-merge:
```bash
npm install clsx tailwind-merge
```
</action>
<verify>
- Callback route handles code exchange
- UI components render without errors
- cn utility works for class merging
</verify>
<done>
Auth callback and reusable UI components created.
</done>
</task>
<task type="auto">
<name>Task 3: Create auth pages (register, login, verify, reset)</name>
<files>
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/components/auth/register-form.tsx
src/components/auth/login-form.tsx
</files>
<action>
Create auth layout at src/app/(auth)/layout.tsx:
```typescript
export default function AuthLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
{children}
</div>
</div>
)
}
```
Create register form component at src/components/auth/register-form.tsx:
```typescript
'use client'
import { useActionState } from 'react'
import { registerUser, 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 RegisterForm() {
const [state, formAction, pending] = useActionState(registerUser, initialState)
if (state.success) {
return (
<div className="text-center">
<div className="mb-4 p-4 bg-green-50 border border-green-200 rounded-md">
<p className="text-green-800">{state.message}</p>
</div>
<Link href="/login" className="text-blue-600 hover:underline">
Torna al login
</Link>
</div>
)
}
return (
<form action={formAction} className="space-y-4">
{state.error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-800 text-sm">{state.error}</p>
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<Input
id="email"
name="email"
type="email"
autoComplete="email"
required
placeholder="tu@esempio.com"
error={!!state.fieldErrors?.email}
/>
{state.fieldErrors?.email && (
<p className="mt-1 text-sm text-red-600">{state.fieldErrors.email[0]}</p>
)}
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<Input
id="password"
name="password"
type="password"
autoComplete="new-password"
required
placeholder="Minimo 8 caratteri"
error={!!state.fieldErrors?.password}
/>
{state.fieldErrors?.password && (
<p className="mt-1 text-sm text-red-600">{state.fieldErrors.password[0]}</p>
)}
<p className="mt-1 text-xs text-gray-500">
Almeno 8 caratteri, 1 numero, 1 maiuscola
</p>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
Conferma Password
</label>
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
autoComplete="new-password"
required
placeholder="Ripeti la password"
error={!!state.fieldErrors?.confirmPassword}
/>
{state.fieldErrors?.confirmPassword && (
<p className="mt-1 text-sm text-red-600">{state.fieldErrors.confirmPassword[0]}</p>
)}
</div>
<Button type="submit" className="w-full" disabled={pending}>
{pending ? 'Registrazione...' : 'Registrati'}
</Button>
<p className="text-center text-sm text-gray-600">
Hai gia un account?{' '}
<Link href="/login" className="text-blue-600 hover:underline">
Accedi
</Link>
</p>
</form>
)
}
```
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 (
<Card>
<CardHeader className="text-center">
<CardTitle>Crea il tuo account</CardTitle>
<CardDescription>
Inizia a usare Leopost gratuitamente
</CardDescription>
</CardHeader>
<CardContent>
<RegisterForm />
</CardContent>
</Card>
)
}
```
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 (
<form action={formAction} className="space-y-4">
{state.error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-800 text-sm">{state.error}</p>
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<Input
id="email"
name="email"
type="email"
autoComplete="email"
required
placeholder="tu@esempio.com"
error={!!state.fieldErrors?.email}
/>
{state.fieldErrors?.email && (
<p className="mt-1 text-sm text-red-600">{state.fieldErrors.email[0]}</p>
)}
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<Input
id="password"
name="password"
type="password"
autoComplete="current-password"
required
placeholder="La tua password"
error={!!state.fieldErrors?.password}
/>
{state.fieldErrors?.password && (
<p className="mt-1 text-sm text-red-600">{state.fieldErrors.password[0]}</p>
)}
</div>
<div className="flex items-center justify-end">
<Link href="/reset-password" className="text-sm text-blue-600 hover:underline">
Password dimenticata?
</Link>
</div>
<Button type="submit" className="w-full" disabled={pending}>
{pending ? 'Accesso...' : 'Accedi'}
</Button>
<p className="text-center text-sm text-gray-600">
Non hai un account?{' '}
<Link href="/register" className="text-blue-600 hover:underline">
Registrati
</Link>
</p>
</form>
)
}
```
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 (
<Card>
<CardHeader className="text-center">
<CardTitle>Accedi a Leopost</CardTitle>
<CardDescription>
Inserisci le tue credenziali per continuare
</CardDescription>
</CardHeader>
<CardContent>
<LoginForm />
</CardContent>
</Card>
)
}
```
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 (
<Card>
<CardHeader className="text-center">
<CardTitle>Verifica la tua email</CardTitle>
<CardDescription>
Ti abbiamo inviato un link di conferma
</CardDescription>
</CardHeader>
<CardContent className="text-center space-y-4">
<p className="text-gray-600">
Controlla la tua casella email e clicca sul link per attivare il tuo account.
</p>
<p className="text-sm text-gray-500">
Non hai ricevuto l'email? Controlla lo spam.
</p>
<Link href="/login" className="text-blue-600 hover:underline">
Torna al login
</Link>
</CardContent>
</Card>
)
}
```
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 (
<Card>
<CardHeader className="text-center">
<CardTitle>Recupera password</CardTitle>
<CardDescription>
Inserisci la tua email per ricevere il link di reset
</CardDescription>
</CardHeader>
<CardContent>
{state.success ? (
<div className="text-center space-y-4">
<div className="p-4 bg-green-50 border border-green-200 rounded-md">
<p className="text-green-800">{state.message}</p>
</div>
<Link href="/login" className="text-blue-600 hover:underline">
Torna al login
</Link>
</div>
) : (
<form action={formAction} className="space-y-4">
{state.error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-800 text-sm">{state.error}</p>
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<Input
id="email"
name="email"
type="email"
autoComplete="email"
required
placeholder="tu@esempio.com"
error={!!state.fieldErrors?.email}
/>
{state.fieldErrors?.email && (
<p className="mt-1 text-sm text-red-600">{state.fieldErrors.email[0]}</p>
)}
</div>
<Button type="submit" className="w-full" disabled={pending}>
{pending ? 'Invio...' : 'Invia link di reset'}
</Button>
<p className="text-center text-sm text-gray-600">
<Link href="/login" className="text-blue-600 hover:underline">
Torna al login
</Link>
</p>
</form>
)}
</CardContent>
</Card>
)
}
```
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 (
<Card>
<CardHeader className="text-center">
<CardTitle>Nuova password</CardTitle>
<CardDescription>
Inserisci la tua nuova password
</CardDescription>
</CardHeader>
<CardContent>
{state.success ? (
<div className="text-center space-y-4">
<div className="p-4 bg-green-50 border border-green-200 rounded-md">
<p className="text-green-800">{state.message}</p>
</div>
<Link href="/login" className="text-blue-600 hover:underline">
Vai al login
</Link>
</div>
) : (
<form action={formAction} className="space-y-4">
{state.error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-800 text-sm">{state.error}</p>
</div>
)}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Nuova Password
</label>
<Input
id="password"
name="password"
type="password"
autoComplete="new-password"
required
placeholder="Minimo 8 caratteri"
error={!!state.fieldErrors?.password}
/>
{state.fieldErrors?.password && (
<p className="mt-1 text-sm text-red-600">{state.fieldErrors.password[0]}</p>
)}
<p className="mt-1 text-xs text-gray-500">
Almeno 8 caratteri, 1 numero, 1 maiuscola
</p>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
Conferma Password
</label>
<Input
id="confirmPassword"
name="confirmPassword"
type="password"
autoComplete="new-password"
required
placeholder="Ripeti la password"
error={!!state.fieldErrors?.confirmPassword}
/>
{state.fieldErrors?.confirmPassword && (
<p className="mt-1 text-sm text-red-600">{state.fieldErrors.confirmPassword[0]}</p>
)}
</div>
<Button type="submit" className="w-full" disabled={pending}>
{pending ? 'Aggiornamento...' : 'Aggiorna password'}
</Button>
</form>
)}
</CardContent>
</Card>
)
}
```
All text is in Italian as per project requirement.
Uses React 19 useActionState (not deprecated useFormState).
</action>
<verify>
- 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
</verify>
<done>
Complete email/password auth flow with register, login, verify, and reset pages.
</done>
</task>
</tasks>
<verification>
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)
</verification>
<success_criteria>
- 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)
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation-auth/01-03-SUMMARY.md`
</output>