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>
This commit is contained in:
554
.planning/phases/01-foundation-auth/01-05-PLAN.md
Normal file
554
.planning/phases/01-foundation-auth/01-05-PLAN.md
Normal file
@@ -0,0 +1,554 @@
|
||||
---
|
||||
phase: 01-foundation-auth
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["01-03", "01-04"]
|
||||
files_modified:
|
||||
- middleware.ts
|
||||
- src/lib/supabase/middleware.ts
|
||||
- src/app/(dashboard)/layout.tsx
|
||||
- src/app/(dashboard)/dashboard/page.tsx
|
||||
- src/components/layout/user-nav.tsx
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "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)"
|
||||
artifacts:
|
||||
- path: "middleware.ts"
|
||||
provides: "Route protection and session refresh"
|
||||
min_lines: 15
|
||||
- path: "src/lib/supabase/middleware.ts"
|
||||
provides: "Supabase session update helper"
|
||||
exports: ["updateSession"]
|
||||
- path: "src/app/(dashboard)/dashboard/page.tsx"
|
||||
provides: "Protected dashboard page"
|
||||
min_lines: 10
|
||||
key_links:
|
||||
- from: "middleware.ts"
|
||||
to: "src/lib/supabase/middleware.ts"
|
||||
via: "updateSession import"
|
||||
pattern: "updateSession"
|
||||
- from: "middleware.ts"
|
||||
to: "Next.js request handling"
|
||||
via: "matcher config"
|
||||
pattern: "matcher.*dashboard"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</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-RESEARCH.md
|
||||
@.planning/phases/01-foundation-auth/01-03-SUMMARY.md
|
||||
@.planning/phases/01-foundation-auth/01-04-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create middleware helper and main middleware</name>
|
||||
<files>
|
||||
src/lib/supabase/middleware.ts
|
||||
middleware.ts
|
||||
</files>
|
||||
<action>
|
||||
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
|
||||
</action>
|
||||
<verify>
|
||||
- middleware.ts exists in project root (not src/)
|
||||
- src/lib/supabase/middleware.ts exists
|
||||
- No TypeScript errors
|
||||
- Matcher pattern is correct
|
||||
</verify>
|
||||
<done>
|
||||
Middleware configured for session refresh and route protection.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create protected dashboard layout and page</name>
|
||||
<files>
|
||||
src/app/(dashboard)/layout.tsx
|
||||
src/app/(dashboard)/dashboard/page.tsx
|
||||
src/components/layout/user-nav.tsx
|
||||
</files>
|
||||
<action>
|
||||
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)
|
||||
</action>
|
||||
<verify>
|
||||
- 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
|
||||
</verify>
|
||||
<done>
|
||||
Protected dashboard with user navigation and plan info display.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Update home page to redirect appropriately</name>
|
||||
<files>
|
||||
src/app/page.tsx
|
||||
</files>
|
||||
<action>
|
||||
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
|
||||
</action>
|
||||
<verify>
|
||||
- Visiting / when logged in redirects to /dashboard
|
||||
- Visiting / when logged out shows landing page
|
||||
- Register and Login buttons work
|
||||
</verify>
|
||||
<done>
|
||||
Home page with auth-aware redirect and landing content.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation-auth/01-05-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user