---
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 (
)
}
)
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 {
error?: boolean
}
const Input = forwardRef(
({ className, error, ...props }, ref) => {
return (
)
}
)
Input.displayName = 'Input'
export { Input }
```
src/components/ui/card.tsx:
```typescript
import { HTMLAttributes, forwardRef } from 'react'
import { cn } from '@/lib/utils'
const Card = forwardRef>(
({ className, ...props }, ref) => (
)
)
Card.displayName = 'Card'
const CardHeader = forwardRef>(
({ className, ...props }, ref) => (
)
)
CardHeader.displayName = 'CardHeader'
const CardTitle = forwardRef>(
({ className, ...props }, ref) => (
)
)
CardTitle.displayName = 'CardTitle'
const CardDescription = forwardRef>(
({ className, ...props }, ref) => (
)
)
CardDescription.displayName = 'CardDescription'
const CardContent = forwardRef>(
({ className, ...props }, ref) => (
)
)
CardContent.displayName = 'CardContent'
const CardFooter = forwardRef>(
({ className, ...props }, ref) => (
)
)
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
```
- Callback route handles code exchange
- UI components render without errors
- cn utility works for class merging
Auth callback and reusable UI components created.
Task 3: Create auth pages (register, login, verify, reset)
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
Create auth layout at src/app/(auth)/layout.tsx:
```typescript
export default function AuthLayout({
children,
}: {
children: React.ReactNode
}) {
return (
{children}
)
}
```
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 (
{state.message}
Torna al login
)
}
return (
)
}
```
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 (
)
}
```
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
) : (
)}
)
}
```
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
) : (
)}
)
}
```
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)