Files
leopost/.planning/phases/01-foundation-auth/01-05-PLAN.md
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

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
01-foundation-auth 05 execute 3
01-03
01-04
middleware.ts
src/lib/supabase/middleware.ts
src/app/(dashboard)/layout.tsx
src/app/(dashboard)/dashboard/page.tsx
src/components/layout/user-nav.tsx
true
truths artifacts key_links
Unauthenticated users are redirected to /login when accessing /dashboard
Authenticated users stay logged in across page refreshes
User can log out and is redirected to login
Session refreshes automatically (no random logouts)
path provides min_lines
middleware.ts Route protection and session refresh 15
path provides exports
src/lib/supabase/middleware.ts Supabase session update helper
updateSession
path provides min_lines
src/app/(dashboard)/dashboard/page.tsx Protected dashboard page 10
from to via pattern
middleware.ts src/lib/supabase/middleware.ts updateSession import updateSession
from to via pattern
middleware.ts Next.js request handling matcher config matcher.*dashboard
Implement middleware for session management and route protection, plus a basic protected dashboard.

Purpose: Ensure authenticated state persists across requests and protect private routes. This is MANDATORY per research - without middleware, sessions expire randomly.

Output: Working route protection where /dashboard requires authentication and sessions auto-refresh.

<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>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-foundation-auth/01-RESEARCH.md @.planning/phases/01-foundation-auth/01-03-SUMMARY.md @.planning/phases/01-foundation-auth/01-04-SUMMARY.md Task 1: Create middleware helper and main middleware src/lib/supabase/middleware.ts middleware.ts Create the middleware helper at src/lib/supabase/middleware.ts:
```typescript
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({
    request,
  })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) =>
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({
            request,
          })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  // IMPORTANT: Do not remove this line
  // Refreshing the auth token is crucial for keeping the session alive
  const { data: { user } } = await supabase.auth.getUser()

  return { supabaseResponse, user }
}
```

Create the main middleware at middleware.ts (project root, NOT in src):

```typescript
import { type NextRequest, NextResponse } from 'next/server'
import { updateSession } from '@/lib/supabase/middleware'

// Routes that require authentication
const protectedRoutes = ['/dashboard', '/settings', '/subscription']

// Routes that should redirect to dashboard if already authenticated
const authRoutes = ['/login', '/register']

export async function middleware(request: NextRequest) {
  const { supabaseResponse, user } = await updateSession(request)
  const { pathname } = request.nextUrl

  // Check if trying to access protected route without auth
  const isProtectedRoute = protectedRoutes.some(route =>
    pathname.startsWith(route)
  )

  if (isProtectedRoute && !user) {
    const redirectUrl = new URL('/login', request.url)
    // Save the original URL to redirect back after login
    redirectUrl.searchParams.set('redirectTo', pathname)
    return NextResponse.redirect(redirectUrl)
  }

  // Check if trying to access auth routes while already authenticated
  const isAuthRoute = authRoutes.some(route =>
    pathname.startsWith(route)
  )

  if (isAuthRoute && user) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return supabaseResponse
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * - public folder files
     */
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}
```

CRITICAL from RESEARCH.md:
- Middleware MUST call supabase.auth.getUser() to refresh session
- Without this, sessions expire and users get randomly logged out
- The matcher excludes static files for performance
- Check path BEFORE redirecting to avoid infinite loops
- middleware.ts exists in project root (not src/) - src/lib/supabase/middleware.ts exists - No TypeScript errors - Matcher pattern is correct Middleware configured for session refresh and route protection. Task 2: Create protected dashboard layout and page src/app/(dashboard)/layout.tsx src/app/(dashboard)/dashboard/page.tsx src/components/layout/user-nav.tsx Create user navigation component at src/components/layout/user-nav.tsx:
```typescript
'use client'

import { createClient } from '@/lib/supabase/client'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { useState } from 'react'

interface UserNavProps {
  email: string
  planName?: string
}

export function UserNav({ email, planName }: UserNavProps) {
  const [loading, setLoading] = useState(false)
  const router = useRouter()
  const supabase = createClient()

  async function handleSignOut() {
    setLoading(true)
    await supabase.auth.signOut()
    router.push('/login')
    router.refresh()
  }

  return (
    <div className="flex items-center gap-4">
      <div className="text-sm text-right">
        <p className="font-medium">{email}</p>
        {planName && (
          <p className="text-gray-500 text-xs capitalize">Piano {planName}</p>
        )}
      </div>
      <Button
        variant="outline"
        size="sm"
        onClick={handleSignOut}
        disabled={loading}
      >
        {loading ? 'Uscita...' : 'Esci'}
      </Button>
    </div>
  )
}
```

Create dashboard layout at src/app/(dashboard)/layout.tsx:

```typescript
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { UserNav } from '@/components/layout/user-nav'
import Link from 'next/link'

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  const supabase = await createClient()

  // Get user (should always exist due to middleware)
  const { data: { user }, error: authError } = await supabase.auth.getUser()

  if (authError || !user) {
    redirect('/login')
  }

  // Get profile with plan info
  const { data: profile } = await supabase
    .from('profiles')
    .select(`
      *,
      plans (
        name,
        display_name_it
      )
    `)
    .eq('id', user.id)
    .single()

  return (
    <div className="min-h-screen bg-gray-50">
      {/* Header */}
      <header className="bg-white border-b border-gray-200">
        <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
          <div className="flex justify-between items-center h-16">
            <div className="flex items-center gap-8">
              <Link href="/dashboard" className="text-xl font-bold text-blue-600">
                Leopost
              </Link>
              <nav className="hidden md:flex items-center gap-4">
                <Link
                  href="/dashboard"
                  className="text-gray-600 hover:text-gray-900 text-sm font-medium"
                >
                  Dashboard
                </Link>
                <Link
                  href="/subscription"
                  className="text-gray-600 hover:text-gray-900 text-sm font-medium"
                >
                  Piano
                </Link>
              </nav>
            </div>
            <UserNav
              email={user.email || ''}
              planName={profile?.plans?.display_name_it}
            />
          </div>
        </div>
      </header>

      {/* Main content */}
      <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
        {children}
      </main>
    </div>
  )
}
```

Create dashboard page at src/app/(dashboard)/dashboard/page.tsx:

```typescript
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'

export default async function DashboardPage() {
  const supabase = await createClient()

  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    redirect('/login')
  }

  // Get profile with plan details
  const { data: profile } = await supabase
    .from('profiles')
    .select(`
      *,
      plans (
        name,
        display_name_it,
        features
      )
    `)
    .eq('id', user.id)
    .single()

  const features = profile?.plans?.features as {
    posts_per_month?: number
    ai_models?: string[]
    social_accounts?: number
  } | null

  return (
    <div className="space-y-6">
      <div>
        <h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
        <p className="text-gray-500">Benvenuto in Leopost</p>
      </div>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        <Card>
          <CardHeader>
            <CardTitle className="text-lg">Il tuo piano</CardTitle>
            <CardDescription>
              {profile?.plans?.display_name_it || 'Gratuito'}
            </CardDescription>
          </CardHeader>
          <CardContent>
            <ul className="space-y-2 text-sm text-gray-600">
              <li>
                <span className="font-medium">{features?.posts_per_month || 10}</span> post/mese
              </li>
              <li>
                <span className="font-medium">{features?.social_accounts || 1}</span> account social
              </li>
              <li>
                <span className="font-medium">{features?.ai_models?.length || 1}</span> modelli AI
              </li>
            </ul>
          </CardContent>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle className="text-lg">Prossimi passi</CardTitle>
            <CardDescription>
              Completa la configurazione
            </CardDescription>
          </CardHeader>
          <CardContent>
            <ul className="space-y-2 text-sm">
              <li className="flex items-center gap-2">
                <span className="w-5 h-5 rounded-full bg-green-100 text-green-600 flex items-center justify-center text-xs">
                  ✓
                </span>
                Account creato
              </li>
              <li className="flex items-center gap-2 text-gray-400">
                <span className="w-5 h-5 rounded-full bg-gray-100 flex items-center justify-center text-xs">
                  2
                </span>
                Collega social (Phase 2)
              </li>
              <li className="flex items-center gap-2 text-gray-400">
                <span className="w-5 h-5 rounded-full bg-gray-100 flex items-center justify-center text-xs">
                  3
                </span>
                Configura brand (Phase 3)
              </li>
            </ul>
          </CardContent>
        </Card>

        <Card>
          <CardHeader>
            <CardTitle className="text-lg">Attivita</CardTitle>
            <CardDescription>
              Le tue statistiche
            </CardDescription>
          </CardHeader>
          <CardContent>
            <div className="text-center py-4 text-gray-400 text-sm">
              Nessuna attivita ancora.
              <br />
              Inizia collegando un account social!
            </div>
          </CardContent>
        </Card>
      </div>
    </div>
  )
}
```

Key points:
- Layout fetches user and profile data server-side
- Dashboard shows plan info from database
- "Prossimi passi" teases future phases
- All text in Italian
- Uses RLS-protected queries (profile data auto-filtered)
- Dashboard layout shows header with navigation - UserNav shows email and plan name - Logout button works - Dashboard page shows plan info - Page redirects to login if not authenticated Protected dashboard with user navigation and plan info display. Task 3: Update home page to redirect appropriately src/app/page.tsx Update the home page to redirect based on auth state.
Modify src/app/page.tsx:

```typescript
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import Link from 'next/link'
import { Button } from '@/components/ui/button'

export default async function Home() {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()

  // If logged in, redirect to dashboard
  if (user) {
    redirect('/dashboard')
  }

  // Landing page for non-authenticated users
  return (
    <main className="min-h-screen flex flex-col items-center justify-center p-8 bg-gradient-to-b from-blue-50 to-white">
      <div className="text-center max-w-2xl">
        <h1 className="text-5xl font-bold text-gray-900 mb-4">
          Leopost
        </h1>
        <p className="text-xl text-gray-600 mb-8">
          Il tuo social media manager potenziato dall'AI.
          <br />
          Minimo sforzo, massima resa.
        </p>

        <div className="flex gap-4 justify-center">
          <Link href="/register">
            <Button size="lg">
              Inizia gratis
            </Button>
          </Link>
          <Link href="/login">
            <Button variant="outline" size="lg">
              Accedi
            </Button>
          </Link>
        </div>

        <p className="mt-8 text-sm text-gray-500">
          Nessuna carta richiesta. Piano gratuito disponibile.
        </p>
      </div>
    </main>
  )
}
```

This creates a simple landing page that:
- Redirects authenticated users to dashboard
- Shows value proposition to visitors
- Provides clear CTAs (register/login)
- Italian copy reflecting the core value
- Visiting / when logged in redirects to /dashboard - Visiting / when logged out shows landing page - Register and Login buttons work Home page with auth-aware redirect and landing content. After all tasks complete: 1. Visit /dashboard when logged out -> redirects to /login 2. Login successfully -> redirects to /dashboard 3. Refresh /dashboard -> stays authenticated (session persists) 4. Click "Esci" -> logs out and redirects to /login 5. Visit / when logged in -> redirects to /dashboard 6. Visit /login when logged in -> redirects to /dashboard

<success_criteria>

  • Middleware refreshes session on every request
  • Protected routes redirect unauthenticated users
  • Auth routes redirect authenticated users
  • Logout works and clears session
  • No infinite redirect loops
  • Dashboard displays user's plan info </success_criteria>
After completion, create `.planning/phases/01-foundation-auth/01-05-SUMMARY.md`