diff --git a/.planning/phases/01-foundation-auth/01-01-PLAN.md b/.planning/phases/01-foundation-auth/01-01-PLAN.md new file mode 100644 index 0000000..419895d --- /dev/null +++ b/.planning/phases/01-foundation-auth/01-01-PLAN.md @@ -0,0 +1,266 @@ +--- +phase: 01-foundation-auth +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - package.json + - tsconfig.json + - next.config.ts + - tailwind.config.ts + - postcss.config.mjs + - .env.local + - .env.example + - src/lib/supabase/client.ts + - src/lib/supabase/server.ts + - src/app/layout.tsx + - src/app/page.tsx + - src/app/globals.css +autonomous: true +user_setup: + - service: supabase + why: "Database and authentication backend" + env_vars: + - name: NEXT_PUBLIC_SUPABASE_URL + source: "Supabase Dashboard -> Project Settings -> API -> Project URL" + - name: NEXT_PUBLIC_SUPABASE_ANON_KEY + source: "Supabase Dashboard -> Project Settings -> API -> anon public" + - name: SUPABASE_SERVICE_ROLE_KEY + source: "Supabase Dashboard -> Project Settings -> API -> service_role (secret)" + +must_haves: + truths: + - "Next.js dev server starts without errors" + - "Supabase client connects to project" + - "Environment variables are loaded correctly" + artifacts: + - path: "src/lib/supabase/client.ts" + provides: "Browser Supabase client for Client Components" + exports: ["createClient"] + - path: "src/lib/supabase/server.ts" + provides: "Server Supabase client for Server Components/Actions" + exports: ["createClient"] + - path: ".env.local" + provides: "Environment configuration" + contains: "NEXT_PUBLIC_SUPABASE_URL" + key_links: + - from: "src/lib/supabase/client.ts" + to: ".env.local" + via: "process.env.NEXT_PUBLIC_*" + pattern: "process\\.env\\.NEXT_PUBLIC_SUPABASE" + - from: "src/lib/supabase/server.ts" + to: "next/headers cookies()" + via: "cookie handling for SSR" + pattern: "cookies\\(\\)" +--- + + +Initialize Next.js 14 project with Supabase client configuration using the official @supabase/ssr package for App Router compatibility. + +Purpose: Establish the foundational project structure and Supabase connectivity that all subsequent auth work depends on. + +Output: Working Next.js 14 app with properly configured Supabase clients (browser + server) ready for auth implementation. + + + +@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-RESEARCH.md + + + + + + Task 1: Initialize Next.js 14 project with TypeScript and Tailwind + + package.json + tsconfig.json + next.config.ts + tailwind.config.ts + postcss.config.mjs + src/app/layout.tsx + src/app/page.tsx + src/app/globals.css + + + Create Next.js 14 project with App Router: + + ```bash + npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" --use-npm + ``` + + If directory not empty, use --force or clean first. + + After init, install Supabase packages: + ```bash + npm install @supabase/supabase-js @supabase/ssr zod react-hook-form + ``` + + Update src/app/page.tsx to a simple placeholder: + ```tsx + export default function Home() { + return ( +
+

Leopost

+

Setup in corso...

+
+ ) + } + ``` + + Ensure next.config.ts uses the new format (not .js): + ```typescript + import type { NextConfig } from 'next' + + const nextConfig: NextConfig = { + // config options here + } + + export default nextConfig + ``` +
+ + Run `npm run dev` - server starts on localhost:3000 without errors. + Visit http://localhost:3000 - shows Leopost placeholder. + + + Next.js 14 project initialized with TypeScript, Tailwind, App Router, and Supabase packages installed. + +
+ + + Task 2: Configure environment variables + + .env.local + .env.example + .gitignore + + + Create .env.example (committed to git, template for others): + ``` + # Supabase Configuration + NEXT_PUBLIC_SUPABASE_URL=your-project-url + NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key + SUPABASE_SERVICE_ROLE_KEY=your-service-role-key + + # App Configuration + NEXT_PUBLIC_APP_URL=http://localhost:3000 + ``` + + Create .env.local with actual Supabase credentials: + - Get URL from Supabase Dashboard -> Project Settings -> API -> Project URL + - Get anon key from Supabase Dashboard -> Project Settings -> API -> anon public + - Get service_role key from Supabase Dashboard -> Project Settings -> API -> service_role + + NOTE: If Supabase project doesn't exist yet, create placeholder values and document that user must create project. + + Verify .gitignore includes: + ``` + .env*.local + ``` + (This should already be present from create-next-app) + + + - .env.example exists and is NOT in .gitignore + - .env.local exists and IS gitignored + - Run `git status` - .env.local should NOT appear + + + Environment files configured with Supabase credentials (or placeholders if project not yet created). + + + + + Task 3: Create Supabase client utilities (dual client pattern) + + src/lib/supabase/client.ts + src/lib/supabase/server.ts + + + Create src/lib/supabase/client.ts for Client Components: + ```typescript + import { createBrowserClient } from '@supabase/ssr' + + export function createClient() { + return createBrowserClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! + ) + } + ``` + + Create src/lib/supabase/server.ts for Server Components, Route Handlers, Server Actions: + ```typescript + import { createServerClient } from '@supabase/ssr' + import { cookies } from 'next/headers' + + export async function createClient() { + const cookieStore = await cookies() + + return createServerClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { + cookies: { + getAll() { + return cookieStore.getAll() + }, + setAll(cookiesToSet) { + try { + cookiesToSet.forEach(({ name, value, options }) => + cookieStore.set(name, value, options) + ) + } catch { + // The `setAll` method was called from a Server Component. + // This can be ignored if you have middleware refreshing user sessions. + } + }, + }, + } + ) + } + ``` + + IMPORTANT: + - Use @supabase/ssr NOT @supabase/auth-helpers-nextjs (deprecated) + - Server client uses async cookies() (Next.js 15 requirement) + - The try/catch in setAll is required for Server Components + + + - Both files exist with correct exports + - `npm run build` completes without TypeScript errors + - No import errors in IDE + + + Dual Supabase client pattern implemented: client.ts for browser, server.ts for SSR/Server Actions. + + + +
+ + +After all tasks complete: +1. `npm run dev` starts without errors +2. `npm run build` completes successfully +3. Both Supabase client files exist and export createClient +4. Environment variables are configured (even if placeholder) +5. Project structure follows Next.js 14 App Router conventions + + + +- Next.js 14 development server runs at localhost:3000 +- Supabase packages installed: @supabase/supabase-js, @supabase/ssr +- Form packages installed: zod, react-hook-form +- Dual client pattern implemented (client.ts + server.ts) +- Environment configuration ready for Supabase connection + + + +After completion, create `.planning/phases/01-foundation-auth/01-01-SUMMARY.md` + diff --git a/.planning/phases/01-foundation-auth/01-02-PLAN.md b/.planning/phases/01-foundation-auth/01-02-PLAN.md new file mode 100644 index 0000000..d3cc892 --- /dev/null +++ b/.planning/phases/01-foundation-auth/01-02-PLAN.md @@ -0,0 +1,450 @@ +--- +phase: 01-foundation-auth +plan: 02 +type: execute +wave: 1 +depends_on: [] +files_modified: + - supabase/migrations/001_initial_auth_setup.sql + - supabase/seed.sql + - docs/DATABASE.md +autonomous: true + +must_haves: + truths: + - "Plans table exists with Free, Creator, Pro entries" + - "Profiles table creates automatically on user signup" + - "RLS policies prevent cross-tenant data access" + - "User cannot see other users' profiles" + artifacts: + - path: "supabase/migrations/001_initial_auth_setup.sql" + provides: "Database schema and RLS policies" + contains: "CREATE TABLE plans" + - path: "docs/DATABASE.md" + provides: "Schema documentation" + contains: "plans" + key_links: + - from: "profiles table" + to: "auth.users" + via: "foreign key + trigger" + pattern: "REFERENCES auth.users" + - from: "profiles table" + to: "plans table" + via: "plan_id foreign key" + pattern: "REFERENCES plans" +--- + + +Create the database schema for multi-tenant authentication with subscription plans and Row Level Security policies. + +Purpose: Establish secure data foundation with tenant isolation from day 1 - this is CRITICAL for security and cannot be retrofitted. + +Output: SQL migration ready to execute in Supabase, with plans table, profiles table, RLS policies, and auto-profile trigger. + + + +@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-RESEARCH.md + + + + + + Task 1: Create database migration with plans and profiles + + supabase/migrations/001_initial_auth_setup.sql + + + Create supabase/migrations/ directory if not exists. + + Create migration file with complete auth schema: + + ```sql + -- Migration: 001_initial_auth_setup.sql + -- Purpose: Create plans, profiles tables with RLS for multi-tenant auth + + -- ============================================ + -- PLANS TABLE + -- ============================================ + + CREATE TABLE public.plans ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name TEXT NOT NULL UNIQUE CHECK (name IN ('free', 'creator', 'pro')), + display_name TEXT NOT NULL, + display_name_it TEXT NOT NULL, -- Italian display name + price_monthly INTEGER NOT NULL CHECK (price_monthly >= 0), -- cents + features JSONB NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ DEFAULT NOW() + ); + + -- Insert default plans + INSERT INTO public.plans (name, display_name, display_name_it, price_monthly, features) VALUES + ('free', 'Free', 'Gratuito', 0, '{ + "posts_per_month": 10, + "ai_models": ["gpt-4o-mini"], + "social_accounts": 1, + "image_generation": false, + "automation": false + }'), + ('creator', 'Creator', 'Creator', 1900, '{ + "posts_per_month": 50, + "ai_models": ["gpt-4o-mini", "gpt-4o", "claude-3-5-sonnet"], + "social_accounts": 3, + "image_generation": true, + "automation": "manual" + }'), + ('pro', 'Pro', 'Pro', 4900, '{ + "posts_per_month": 200, + "ai_models": ["gpt-4o-mini", "gpt-4o", "claude-3-5-sonnet", "claude-opus-4"], + "social_accounts": 10, + "image_generation": true, + "automation": "full" + }'); + + -- ============================================ + -- PROFILES TABLE + -- ============================================ + + CREATE TABLE public.profiles ( + id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + tenant_id UUID NOT NULL DEFAULT gen_random_uuid(), + plan_id UUID REFERENCES public.plans(id) NOT NULL DEFAULT (SELECT id FROM public.plans WHERE name = 'free'), + email TEXT NOT NULL, + full_name TEXT, + avatar_url TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() + ); + + -- Performance indexes + CREATE INDEX idx_profiles_tenant_id ON public.profiles(tenant_id); + CREATE INDEX idx_profiles_plan_id ON public.profiles(plan_id); + CREATE INDEX idx_profiles_email ON public.profiles(email); + + -- ============================================ + -- ROW LEVEL SECURITY + -- ============================================ + + -- Enable RLS on all tables (CRITICAL - never skip this) + ALTER TABLE public.plans ENABLE ROW LEVEL SECURITY; + ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; + + -- Plans: Everyone can read (public info) + CREATE POLICY "Plans are viewable by everyone" + ON public.plans FOR SELECT + TO authenticated, anon + USING (true); + + -- Profiles: Users can only read their own profile + -- IMPORTANT: Use (SELECT auth.uid()) for 99% performance improvement + CREATE POLICY "Users can read own profile" + ON public.profiles FOR SELECT + TO authenticated + USING ((SELECT auth.uid()) = id); + + -- Profiles: Users can update their own profile + CREATE POLICY "Users can update own profile" + ON public.profiles FOR UPDATE + TO authenticated + USING ((SELECT auth.uid()) = id) + WITH CHECK ((SELECT auth.uid()) = id); + + -- Profiles: System can insert (via trigger) + -- Note: INSERT policy needed because trigger runs as SECURITY DEFINER + CREATE POLICY "System can insert profiles" + ON public.profiles FOR INSERT + TO authenticated + WITH CHECK ((SELECT auth.uid()) = id); + + -- ============================================ + -- AUTO-CREATE PROFILE TRIGGER + -- ============================================ + + -- Function to create profile on user signup + CREATE OR REPLACE FUNCTION public.handle_new_user() + RETURNS TRIGGER AS $$ + BEGIN + INSERT INTO public.profiles (id, email, tenant_id, full_name, avatar_url) + VALUES ( + NEW.id, + NEW.email, + gen_random_uuid(), -- Each user gets unique tenant_id + COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.raw_user_meta_data->>'name'), + NEW.raw_user_meta_data->>'avatar_url' + ); + RETURN NEW; + END; + $$ LANGUAGE plpgsql SECURITY DEFINER; + + -- Trigger on auth.users insert + CREATE TRIGGER on_auth_user_created + AFTER INSERT ON auth.users + FOR EACH ROW + EXECUTE FUNCTION public.handle_new_user(); + + -- ============================================ + -- HELPER FUNCTIONS + -- ============================================ + + -- Function to get current user's plan features (for API limit checking) + CREATE OR REPLACE FUNCTION public.get_user_plan_features() + RETURNS JSONB + LANGUAGE SQL STABLE + SECURITY DEFINER + AS $$ + SELECT p.features + FROM public.plans p + INNER JOIN public.profiles pr ON pr.plan_id = p.id + WHERE pr.id = (SELECT auth.uid()); + $$; + + -- Function to get current user's plan name + CREATE OR REPLACE FUNCTION public.get_user_plan_name() + RETURNS TEXT + LANGUAGE SQL STABLE + SECURITY DEFINER + AS $$ + SELECT p.name + FROM public.plans p + INNER JOIN public.profiles pr ON pr.plan_id = p.id + WHERE pr.id = (SELECT auth.uid()); + $$; + + -- Function to update profile's updated_at timestamp + CREATE OR REPLACE FUNCTION public.update_updated_at() + RETURNS TRIGGER AS $$ + BEGIN + NEW.updated_at = NOW(); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER profiles_updated_at + BEFORE UPDATE ON public.profiles + FOR EACH ROW + EXECUTE FUNCTION public.update_updated_at(); + + -- ============================================ + -- GRANTS + -- ============================================ + + -- Grant usage to authenticated users + GRANT USAGE ON SCHEMA public TO authenticated; + GRANT SELECT ON public.plans TO authenticated; + GRANT SELECT, UPDATE ON public.profiles TO authenticated; + GRANT EXECUTE ON FUNCTION public.get_user_plan_features() TO authenticated; + GRANT EXECUTE ON FUNCTION public.get_user_plan_name() TO authenticated; + ``` + + CRITICAL NOTES from RESEARCH.md: + - RLS MUST be enabled on EVERY table (CVE-2025-48757 exposed 170+ apps without this) + - Use (SELECT auth.uid()) not bare auth.uid() for 99% performance improvement + - Both SELECT and INSERT policies needed for profiles (PostgreSQL returns inserted rows) + - SECURITY DEFINER on functions to bypass RLS when needed + + + - File exists at supabase/migrations/001_initial_auth_setup.sql + - SQL syntax is valid (no obvious errors) + - All three plans are inserted (free, creator, pro) + - RLS is enabled on both tables + - Trigger function exists for auto-profile creation + + + Complete database migration ready for Supabase execution. + + + + + Task 2: Create seed file for development + + supabase/seed.sql + + + Create seed file for development/testing (optional data beyond migration): + + ```sql + -- Seed file for development + -- Note: Plans are already seeded in migration + -- This file is for additional test data if needed + + -- Verify plans exist + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM public.plans WHERE name = 'free') THEN + RAISE EXCEPTION 'Plans not found - run migration first'; + END IF; + END $$; + + -- Log seed completion + DO $$ + BEGIN + RAISE NOTICE 'Seed completed. Plans available: free, creator, pro'; + END $$; + ``` + + This seed file is minimal because: + - Plans are created in migration (should always exist) + - Profiles are created automatically via trigger + - Test users should be created through the app flow + + + File exists at supabase/seed.sql + + + Seed file created for development verification. + + + + + Task 3: Document database schema + + docs/DATABASE.md + + + Create docs/ directory if not exists. + + Create DATABASE.md documenting the schema: + + ```markdown + # Database Schema - Leopost + + ## Overview + + Leopost uses Supabase (PostgreSQL) with Row Level Security for multi-tenant data isolation. + + ## Tables + + ### plans + + Subscription plan definitions. + + | Column | Type | Description | + |--------|------|-------------| + | id | UUID | Primary key | + | name | TEXT | Unique identifier: 'free', 'creator', 'pro' | + | display_name | TEXT | English display name | + | display_name_it | TEXT | Italian display name | + | price_monthly | INTEGER | Price in cents (0, 1900, 4900) | + | features | JSONB | Feature limits and flags | + | created_at | TIMESTAMPTZ | Creation timestamp | + + **Features JSONB structure:** + ```json + { + "posts_per_month": 10, + "ai_models": ["gpt-4o-mini"], + "social_accounts": 1, + "image_generation": false, + "automation": false + } + ``` + + ### profiles + + User profiles with tenant isolation. + + | Column | Type | Description | + |--------|------|-------------| + | id | UUID | Primary key, references auth.users | + | tenant_id | UUID | Tenant isolation key (auto-generated) | + | plan_id | UUID | References plans.id, defaults to 'free' | + | email | TEXT | User email | + | full_name | TEXT | Optional display name | + | avatar_url | TEXT | Optional avatar URL | + | created_at | TIMESTAMPTZ | Creation timestamp | + | updated_at | TIMESTAMPTZ | Last update timestamp | + + ## Row Level Security + + **CRITICAL**: RLS is enabled on all tables. Never bypass RLS in client code. + + ### plans + - SELECT: Everyone (authenticated + anon) can read + + ### profiles + - SELECT: Users can only read their own profile + - UPDATE: Users can only update their own profile + - INSERT: System creates via trigger on signup + + ## Helper Functions + + ### get_user_plan_features() + Returns JSONB of current user's plan features. Use for limit checking. + + ```typescript + const { data } = await supabase.rpc('get_user_plan_features') + // Returns: { posts_per_month: 10, ai_models: [...], ... } + ``` + + ### get_user_plan_name() + Returns TEXT of current user's plan name ('free', 'creator', 'pro'). + + ## Triggers + + ### on_auth_user_created + Automatically creates a profile when a new user signs up via Supabase Auth. + - Sets tenant_id to new UUID (multi-tenant isolation) + - Sets plan_id to 'free' plan + - Copies email, full_name, avatar_url from auth metadata + + ## Running Migrations + + Option 1: Supabase Dashboard + 1. Go to SQL Editor + 2. Paste migration content + 3. Run + + Option 2: Supabase CLI + ```bash + supabase db push + ``` + + ## Security Notes + + - **Never** use service_role key in client code + - **Always** verify RLS is enabled after schema changes + - Use Supabase Security Advisor in dashboard before production + - tenant_id is in profiles table, not JWT (simpler approach for v1) + ``` + + + - docs/DATABASE.md exists + - Documents both tables + - Includes RLS policies + - Includes helper functions + + + Database schema documented for team reference. + + + + + + +After all tasks complete: +1. Migration file exists and contains valid SQL +2. Plans table has 3 entries (free, creator, pro) +3. Profiles table has RLS policies +4. Trigger creates profile on user signup +5. Helper functions exist for plan checking +6. Documentation is complete + + + +- SQL migration ready to execute in Supabase +- RLS enabled on ALL tables (security critical) +- Auto-profile creation via trigger +- Plan features stored as JSONB for flexibility +- Helper functions for limit checking +- Schema documented in docs/DATABASE.md + + + +After completion, create `.planning/phases/01-foundation-auth/01-02-SUMMARY.md` + diff --git a/.planning/phases/01-foundation-auth/01-03-PLAN.md b/.planning/phases/01-foundation-auth/01-03-PLAN.md new file mode 100644 index 0000000..2c44b7d --- /dev/null +++ b/.planning/phases/01-foundation-auth/01-03-PLAN.md @@ -0,0 +1,1029 @@ +--- +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` + diff --git a/.planning/phases/01-foundation-auth/01-04-PLAN.md b/.planning/phases/01-foundation-auth/01-04-PLAN.md new file mode 100644 index 0000000..394f9b7 --- /dev/null +++ b/.planning/phases/01-foundation-auth/01-04-PLAN.md @@ -0,0 +1,450 @@ +--- +phase: 01-foundation-auth +plan: 04 +type: execute +wave: 2 +depends_on: ["01-01", "01-02"] +files_modified: + - src/components/auth/google-button.tsx + - src/app/(auth)/login/page.tsx + - src/app/(auth)/register/page.tsx + - docs/GOOGLE_OAUTH_SETUP.md +autonomous: true +user_setup: + - service: google-cloud + why: "Google OAuth for social login" + env_vars: [] + dashboard_config: + - task: "Create OAuth 2.0 Client ID" + location: "Google Cloud Console -> APIs & Services -> Credentials" + - task: "Add authorized JavaScript origins" + location: "OAuth Client -> Authorized JavaScript origins" + value: "http://localhost:3000, https://your-domain.com" + - task: "Add authorized redirect URI" + location: "OAuth Client -> Authorized redirect URIs" + value: "https://.supabase.co/auth/v1/callback" + - service: supabase + why: "Enable Google OAuth provider" + env_vars: [] + dashboard_config: + - task: "Enable Google provider" + location: "Supabase Dashboard -> Authentication -> Providers -> Google" + - task: "Paste Google Client ID and Secret" + location: "Same settings page" + +must_haves: + truths: + - "User can click 'Accedi con Google' button" + - "User is redirected to Google consent screen" + - "User returns to app authenticated after consent" + - "User session persists after Google login" + artifacts: + - path: "src/components/auth/google-button.tsx" + provides: "Google Sign-In button component" + exports: ["GoogleSignInButton"] + - path: "docs/GOOGLE_OAUTH_SETUP.md" + provides: "Setup instructions for Google OAuth" + contains: "Google Cloud Console" + key_links: + - from: "src/components/auth/google-button.tsx" + to: "Supabase Auth" + via: "signInWithOAuth" + pattern: "signInWithOAuth.*google" + - from: "Google OAuth" + to: "src/app/auth/callback/route.ts" + via: "redirect after consent" + pattern: "auth/callback" +--- + + +Implement Google OAuth login allowing users to sign in with their Google account. + +Purpose: Enable social login per AUTH-02 requirement. Google OAuth provides frictionless registration/login. + +Output: Working Google Sign-In button that authenticates users and creates their profile automatically. + + + +@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-RESEARCH.md +@.planning/phases/01-foundation-auth/01-01-SUMMARY.md +@.planning/phases/01-foundation-auth/01-02-SUMMARY.md + + + + + + Task 1: Create Google Sign-In button component + + src/components/auth/google-button.tsx + + + Create the Google Sign-In button at src/components/auth/google-button.tsx: + + ```typescript + 'use client' + + import { createClient } from '@/lib/supabase/client' + import { Button } from '@/components/ui/button' + import { useState } from 'react' + + // Simple Google icon SVG + function GoogleIcon({ className }: { className?: string }) { + return ( + + + + + + + ) + } + + export function GoogleSignInButton() { + const [loading, setLoading] = useState(false) + const supabase = createClient() + + async function handleGoogleSignIn() { + setLoading(true) + + const { error } = await supabase.auth.signInWithOAuth({ + provider: 'google', + options: { + redirectTo: `${window.location.origin}/auth/callback`, + queryParams: { + access_type: 'offline', + prompt: 'consent', + }, + }, + }) + + if (error) { + console.error('Google sign-in error:', error) + setLoading(false) + } + // Note: No need to handle success - user is redirected to Google + } + + return ( + + ) + } + ``` + + Key points: + - Uses 'use client' since it needs browser APIs + - Uses createClient from lib/supabase/client.ts (browser client) + - redirectTo points to /auth/callback which handles the code exchange + - access_type: 'offline' requests refresh token + - prompt: 'consent' ensures user always sees consent screen (good for debugging) + - Italian button text per project requirement + + + - Component file exists + - No TypeScript errors + - Button renders with Google icon + + + Google Sign-In button component created with proper OAuth configuration. + + + + + Task 2: Add Google button to login and register pages + + src/app/(auth)/login/page.tsx + src/app/(auth)/register/page.tsx + src/components/auth/login-form.tsx + src/components/auth/register-form.tsx + + + Update login page to include Google button. Modify src/app/(auth)/login/page.tsx: + + ```typescript + import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' + import { LoginForm } from '@/components/auth/login-form' + import { GoogleSignInButton } from '@/components/auth/google-button' + + export default function LoginPage() { + return ( + + + Accedi a Leopost + + Inserisci le tue credenziali per continuare + + + + + +
+
+ +
+
+ oppure +
+
+ + +
+
+ ) + } + ``` + + Update register page similarly. Modify src/app/(auth)/register/page.tsx: + + ```typescript + import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' + import { RegisterForm } from '@/components/auth/register-form' + import { GoogleSignInButton } from '@/components/auth/google-button' + + export default function RegisterPage() { + return ( + + + Crea il tuo account + + Inizia a usare Leopost gratuitamente + + + + + +
+
+ +
+
+ oppure +
+
+ + +
+
+ ) + } + ``` + + The divider uses "oppure" (Italian for "or") to separate social login from email form. + Google button appears FIRST (above the form) as it's the faster option. +
+ + - Login page shows Google button above email form + - Register page shows Google button above email form + - "oppure" divider separates the two methods + - Pages render without errors + + + Google Sign-In button integrated into login and registration pages. + +
+ + + Task 3: Document Google OAuth setup process + + docs/GOOGLE_OAUTH_SETUP.md + + + Create comprehensive setup documentation at docs/GOOGLE_OAUTH_SETUP.md: + + ```markdown + # Google OAuth Setup Guide + + This guide explains how to configure Google OAuth for Leopost. + + ## Prerequisites + + - Google Cloud account + - Supabase project created + - Access to Supabase Dashboard + + ## Step 1: Google Cloud Console Setup + + 1. Go to [Google Cloud Console](https://console.cloud.google.com/) + + 2. Create a new project or select existing one + + 3. Navigate to **APIs & Services > Credentials** + + 4. Click **Create Credentials > OAuth client ID** + + 5. If prompted, configure the OAuth consent screen: + - User Type: External + - App name: Leopost + - User support email: your email + - Developer contact: your email + - Scopes: email, profile, openid (default) + + 6. Create OAuth Client ID: + - Application type: **Web application** + - Name: Leopost Web Client + + 7. Add **Authorized JavaScript origins**: + ``` + http://localhost:3000 + https://your-production-domain.com + ``` + + 8. Add **Authorized redirect URIs**: + ``` + https://.supabase.co/auth/v1/callback + ``` + + Find your project ref in Supabase Dashboard > Project Settings > General + + 9. Click **Create** and save the **Client ID** and **Client Secret** + + ## Step 2: Supabase Configuration + + 1. Go to [Supabase Dashboard](https://supabase.com/dashboard) + + 2. Select your project + + 3. Navigate to **Authentication > Providers** + + 4. Find **Google** and click to enable + + 5. Enter: + - **Client ID**: From Google Cloud Console + - **Client Secret**: From Google Cloud Console + + 6. Copy the **Callback URL** shown (should match what you entered in Google) + + 7. Click **Save** + + ## Step 3: Environment Variables + + No additional environment variables needed for Google OAuth. + The configuration is stored in Supabase Dashboard. + + Your existing `.env.local` should have: + ``` + NEXT_PUBLIC_SUPABASE_URL=your-project-url + NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key + ``` + + ## Step 4: Test the Integration + + 1. Start development server: `npm run dev` + + 2. Go to http://localhost:3000/login + + 3. Click "Accedi con Google" + + 4. You should be redirected to Google consent screen + + 5. After consent, you should return to the app authenticated + + 6. Check Supabase Dashboard > Authentication > Users to verify user was created + + ## Troubleshooting + + ### "redirect_uri_mismatch" error + + The redirect URI in Google Console doesn't match Supabase's callback URL. + - Verify the exact URL in Supabase Auth Providers matches Google Console + - Check for trailing slashes + - Ensure using HTTPS for Supabase callback + + ### "Access blocked: This app's request is invalid" + + OAuth consent screen not configured or not published. + - Configure OAuth consent screen in Google Cloud Console + - For testing, add your email as a test user + - For production, submit for verification + + ### User created but profile missing + + The database trigger might not have fired. + - Check if `on_auth_user_created` trigger exists + - Verify `handle_new_user` function has correct permissions + - Check Supabase logs for errors + + ### Session not persisting + + Middleware might not be refreshing sessions. + - Ensure middleware.ts is in project root + - Check middleware matcher includes auth routes + - Verify cookies are being set correctly + + ## Security Notes + + - Never commit Client Secret to version control + - For production, publish OAuth consent screen for verification + - Use a separate OAuth client for production vs development + - Regularly rotate Client Secrets + + ## Local Development vs Production + + | Setting | Local | Production | + |---------|-------|------------| + | JavaScript origins | http://localhost:3000 | https://your-domain.com | + | Redirect URI | Same Supabase callback | Same Supabase callback | + | Consent screen | Testing mode | Published/Verified | + | Test users | Your email added | Not needed | + ``` + + This documentation helps future developers (or the user) configure Google OAuth correctly. + + + - File exists at docs/GOOGLE_OAUTH_SETUP.md + - Contains step-by-step instructions + - Includes troubleshooting section + - Covers both Google Cloud and Supabase configuration + + + Google OAuth setup documentation created. + + + +
+ + +After all tasks complete: +1. Google button appears on /login and /register pages +2. Clicking button redirects to Google (or shows error if not configured) +3. Documentation provides clear setup steps +4. No TypeScript or build errors + + + +- GoogleSignInButton component exists and renders +- Login/register pages show Google option prominently +- Setup documentation is comprehensive +- OAuth flow uses correct callback URL (/auth/callback) +- Italian text used throughout UI + + + +After completion, create `.planning/phases/01-foundation-auth/01-04-SUMMARY.md` + diff --git a/.planning/phases/01-foundation-auth/01-05-PLAN.md b/.planning/phases/01-foundation-auth/01-05-PLAN.md new file mode 100644 index 0000000..da610be --- /dev/null +++ b/.planning/phases/01-foundation-auth/01-05-PLAN.md @@ -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" +--- + + +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. + + + +@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-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 ( +
+
+

{email}

+ {planName && ( +

Piano {planName}

+ )} +
+ +
+ ) + } + ``` + + 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 ( +
+ {/* Header */} +
+
+
+
+ + Leopost + + +
+ +
+
+
+ + {/* Main content */} +
+ {children} +
+
+ ) + } + ``` + + 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 ( +
+
+

Dashboard

+

Benvenuto in Leopost

+
+ +
+ + + Il tuo piano + + {profile?.plans?.display_name_it || 'Gratuito'} + + + +
    +
  • + {features?.posts_per_month || 10} post/mese +
  • +
  • + {features?.social_accounts || 1} account social +
  • +
  • + {features?.ai_models?.length || 1} modelli AI +
  • +
+
+
+ + + + Prossimi passi + + Completa la configurazione + + + +
    +
  • + + ✓ + + Account creato +
  • +
  • + + 2 + + Collega social (Phase 2) +
  • +
  • + + 3 + + Configura brand (Phase 3) +
  • +
+
+
+ + + + Attivita + + Le tue statistiche + + + +
+ Nessuna attivita ancora. +
+ Inizia collegando un account social! +
+
+
+
+
+ ) + } + ``` + + 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 ( +
+
+

+ Leopost +

+

+ Il tuo social media manager potenziato dall'AI. +
+ Minimo sforzo, massima resa. +

+ +
+ + + + + + +
+ +

+ Nessuna carta richiesta. Piano gratuito disponibile. +

+
+
+ ) + } + ``` + + 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 + + + +- 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 + + + +After completion, create `.planning/phases/01-foundation-auth/01-05-SUMMARY.md` + diff --git a/.planning/phases/01-foundation-auth/01-06-PLAN.md b/.planning/phases/01-foundation-auth/01-06-PLAN.md new file mode 100644 index 0000000..ee1351e --- /dev/null +++ b/.planning/phases/01-foundation-auth/01-06-PLAN.md @@ -0,0 +1,639 @@ +--- +phase: 01-foundation-auth +plan: 06 +type: execute +wave: 4 +depends_on: ["01-05"] +files_modified: + - src/app/(dashboard)/subscription/page.tsx + - src/app/actions/subscription.ts + - src/components/subscription/plan-card.tsx + - src/lib/plans.ts +autonomous: true + +must_haves: + truths: + - "User can view all available plans (Free, Creator, Pro)" + - "User can see their current plan highlighted" + - "User can switch to a different plan" + - "Plan change updates profile in database" + - "Plan features are displayed clearly" + artifacts: + - path: "src/app/(dashboard)/subscription/page.tsx" + provides: "Subscription management page" + min_lines: 30 + - path: "src/app/actions/subscription.ts" + provides: "Server action for plan switching" + exports: ["switchPlan"] + - path: "src/components/subscription/plan-card.tsx" + provides: "Reusable plan display component" + exports: ["PlanCard"] + key_links: + - from: "src/components/subscription/plan-card.tsx" + to: "src/app/actions/subscription.ts" + via: "switchPlan action" + pattern: "switchPlan" + - from: "src/app/actions/subscription.ts" + to: "profiles table" + via: "update plan_id" + pattern: "update.*plan_id" +--- + + +Implement subscription management allowing users to view plans and switch between them. + +Purpose: Enable users to view and change their subscription plan per AUTH-03 requirement. Payment integration deferred to later phase. + +Output: Working subscription page where users can view all plans and switch their plan. + + + +@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-RESEARCH.md +@.planning/phases/01-foundation-auth/01-02-SUMMARY.md +@.planning/phases/01-foundation-auth/01-05-SUMMARY.md + + + + + + Task 1: Create plan utilities and types + + src/lib/plans.ts + src/types/database.ts + + + Create type definitions at src/types/database.ts: + + ```typescript + export interface Plan { + id: string + name: 'free' | 'creator' | 'pro' + display_name: string + display_name_it: string + price_monthly: number + features: PlanFeatures + created_at: string + } + + export interface PlanFeatures { + posts_per_month: number + ai_models: string[] + social_accounts: number + image_generation: boolean + automation: boolean | 'manual' | 'full' + } + + export interface Profile { + id: string + tenant_id: string + plan_id: string + email: string + full_name: string | null + avatar_url: string | null + created_at: string + updated_at: string + plans?: Plan + } + ``` + + Create plan utilities at src/lib/plans.ts: + + ```typescript + import { PlanFeatures } from '@/types/database' + + export const PLAN_DISPLAY_ORDER = ['free', 'creator', 'pro'] as const + + // Feature display names in Italian + export const FEATURE_LABELS: Record = { + posts_per_month: 'Post al mese', + ai_models: 'Modelli AI', + social_accounts: 'Account social', + image_generation: 'Generazione immagini', + automation: 'Automazione', + } + + export function formatFeatureValue( + key: keyof PlanFeatures, + value: PlanFeatures[keyof PlanFeatures] + ): string { + if (typeof value === 'boolean') { + return value ? 'Incluso' : 'Non incluso' + } + + if (Array.isArray(value)) { + return value.length.toString() + } + + if (key === 'automation') { + if (value === 'manual') return 'Solo manuale' + if (value === 'full') return 'Completa' + return 'Non inclusa' + } + + return value.toString() + } + + export function formatPrice(cents: number): string { + if (cents === 0) return 'Gratis' + return `€${(cents / 100).toFixed(0)}/mese` + } + + export function getPlanBadgeColor(planName: string): string { + switch (planName) { + case 'pro': + return 'bg-purple-100 text-purple-800 border-purple-200' + case 'creator': + return 'bg-blue-100 text-blue-800 border-blue-200' + default: + return 'bg-gray-100 text-gray-800 border-gray-200' + } + } + ``` + + These utilities: + - Define TypeScript types for plans + - Provide Italian labels for features + - Format prices and feature values + - Handle plan badge colors + + + - Both files exist + - Types are correctly defined + - Utility functions work + - No TypeScript errors + + + Plan types and utility functions created. + + + + + Task 2: Create plan card component and switch action + + src/components/subscription/plan-card.tsx + src/app/actions/subscription.ts + + + Create plan card component at src/components/subscription/plan-card.tsx: + + ```typescript + 'use client' + + import { Plan, PlanFeatures } from '@/types/database' + import { formatFeatureValue, formatPrice, FEATURE_LABELS, getPlanBadgeColor } from '@/lib/plans' + import { Button } from '@/components/ui/button' + import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' + import { switchPlan } from '@/app/actions/subscription' + import { useTransition } from 'react' + + interface PlanCardProps { + plan: Plan + isCurrentPlan: boolean + onPlanChange?: () => void + } + + export function PlanCard({ plan, isCurrentPlan, onPlanChange }: PlanCardProps) { + const [isPending, startTransition] = useTransition() + + const features = plan.features as PlanFeatures + + function handleSwitchPlan() { + startTransition(async () => { + const result = await switchPlan(plan.id) + if (result.success && onPlanChange) { + onPlanChange() + } + }) + } + + // Highlight features to show + const displayFeatures: (keyof PlanFeatures)[] = [ + 'posts_per_month', + 'social_accounts', + 'ai_models', + 'image_generation', + 'automation', + ] + + return ( + + {isCurrentPlan && ( +
+ + Piano attuale + +
+ )} + + +
+ + {plan.display_name_it} + +
+ + {formatPrice(plan.price_monthly)} + + + {plan.name === 'free' && 'Perfetto per iniziare'} + {plan.name === 'creator' && 'Per creator seri'} + {plan.name === 'pro' && 'Per professionisti'} + +
+ + +
    + {displayFeatures.map((featureKey) => { + const value = features[featureKey] + const isIncluded = value !== false && value !== 'non incluso' + + return ( +
  • + + {isIncluded ? '✓' : '—'} + + + + {formatFeatureValue(featureKey, value)} + + {' '} + + {FEATURE_LABELS[featureKey].toLowerCase()} + + +
  • + ) + })} +
+
+ + + {isCurrentPlan ? ( + + ) : ( + + )} + +
+ ) + } + ``` + + Create subscription action at src/app/actions/subscription.ts: + + ```typescript + 'use server' + + import { createClient } from '@/lib/supabase/server' + import { revalidatePath } from 'next/cache' + + export type SubscriptionActionState = { + success?: boolean + error?: string + message?: string + } + + export async function switchPlan(planId: string): Promise { + const supabase = await createClient() + + // Get current user + const { data: { user }, error: authError } = await supabase.auth.getUser() + + if (authError || !user) { + return { error: 'Devi essere autenticato per cambiare piano' } + } + + // Verify plan exists + const { data: plan, error: planError } = await supabase + .from('plans') + .select('id, name, display_name_it') + .eq('id', planId) + .single() + + if (planError || !plan) { + return { error: 'Piano non trovato' } + } + + // Update user's plan + const { error: updateError } = await supabase + .from('profiles') + .update({ plan_id: planId }) + .eq('id', user.id) + + if (updateError) { + console.error('Failed to update plan:', updateError) + return { error: 'Errore durante il cambio piano. Riprova.' } + } + + // Revalidate pages that show plan info + revalidatePath('/dashboard') + revalidatePath('/subscription') + + return { + success: true, + message: `Piano cambiato a ${plan.display_name_it}` + } + } + + export async function getCurrentPlan() { + const supabase = await createClient() + + const { data: { user } } = await supabase.auth.getUser() + + if (!user) { + return null + } + + const { data: profile } = await supabase + .from('profiles') + .select(` + plan_id, + plans ( + id, + name, + display_name, + display_name_it, + price_monthly, + features + ) + `) + .eq('id', user.id) + .single() + + return profile?.plans || null + } + ``` + + NOTE: This is a simplified plan switching for v1. In production: + - Payment would be processed before switching to paid plans + - Downgrade might be scheduled for end of billing period + - Proration logic would be needed + - These complexities are deferred per CONTEXT.md +
+ + - PlanCard component renders all plan features + - switchPlan action updates database + - Current plan is highlighted + - Non-current plans have switch button + + + Plan card component and switch action created. + +
+ + + Task 3: Create subscription page + + src/app/(dashboard)/subscription/page.tsx + + + Create subscription management page at src/app/(dashboard)/subscription/page.tsx: + + ```typescript + import { createClient } from '@/lib/supabase/server' + import { redirect } from 'next/navigation' + import { PlanCard } from '@/components/subscription/plan-card' + import { Plan } from '@/types/database' + import { PLAN_DISPLAY_ORDER } from '@/lib/plans' + + export default async function SubscriptionPage() { + const supabase = await createClient() + + const { data: { user } } = await supabase.auth.getUser() + + if (!user) { + redirect('/login') + } + + // Get user's current plan + const { data: profile } = await supabase + .from('profiles') + .select('plan_id') + .eq('id', user.id) + .single() + + // Get all plans + const { data: plans, error: plansError } = await supabase + .from('plans') + .select('*') + .order('price_monthly', { ascending: true }) + + if (plansError || !plans) { + return ( +
+

Errore nel caricamento dei piani

+
+ ) + } + + // Sort plans by our display order + const sortedPlans = [...plans].sort((a, b) => { + return PLAN_DISPLAY_ORDER.indexOf(a.name as typeof PLAN_DISPLAY_ORDER[number]) - + PLAN_DISPLAY_ORDER.indexOf(b.name as typeof PLAN_DISPLAY_ORDER[number]) + }) + + return ( +
+
+

Il tuo abbonamento

+

+ Scegli il piano piu adatto alle tue esigenze +

+
+ + {/* Info banner */} +
+

+ Nota: Il pagamento verra implementato nelle prossime versioni. + Per ora puoi passare liberamente tra i piani per testare le funzionalita. +

+
+ + {/* Plans grid */} +
+ {sortedPlans.map((plan) => ( + + ))} +
+ + {/* Feature comparison */} +
+

+ Confronto funzionalita +

+
+ + + + + {sortedPlans.map((plan) => ( + + ))} + + + + (p.features as Plan['features']).posts_per_month.toString()} + /> + (p.features as Plan['features']).social_accounts.toString()} + /> + (p.features as Plan['features']).ai_models.length.toString()} + /> + (p.features as Plan['features']).image_generation ? '✓' : '—'} + /> + { + const auto = (p.features as Plan['features']).automation + if (auto === false) return '—' + if (auto === 'manual') return 'Manuale' + if (auto === 'full') return 'Completa' + return '—' + }} + /> + +
+ Funzionalita + + {plan.display_name_it} +
+
+
+ + {/* FAQ */} +
+

+ Domande frequenti +

+
+ + + +
+
+
+ ) + } + + function FeatureRow({ + feature, + plans, + getValue, + }: { + feature: string + plans: Plan[] + getValue: (plan: Plan) => string + }) { + return ( + + {feature} + {plans.map((plan) => ( + + {getValue(plan)} + + ))} + + ) + } + + function FaqItem({ question, answer }: { question: string; answer: string }) { + return ( +
+

{question}

+

{answer}

+
+ ) + } + ``` + + This page: + - Shows all three plans in cards + - Highlights current plan + - Allows switching between plans + - Shows feature comparison table + - Includes FAQ section + - Notes that payment is deferred (transparency) + - All text in Italian +
+ + - Page loads at /subscription + - All three plans display + - Current plan is highlighted + - Can click to switch plans + - Feature comparison table is accurate + + + Complete subscription management page with plan display and switching. + +
+ +
+ + +After all tasks complete: +1. Visit /subscription when logged in +2. See all three plans (Free, Creator, Pro) +3. Current plan is highlighted with "Piano attuale" badge +4. Click switch button on different plan -> plan changes +5. Dashboard reflects new plan after switch +6. Feature comparison table shows correct values + + + +- All three plans display with correct pricing +- User can view their current plan +- User can switch to a different plan +- Plan switch updates database (verify in Supabase) +- Feature limits are clearly displayed +- All text is in Italian +- Note about payment deferral is visible + + + +After completion, create `.planning/phases/01-foundation-auth/01-06-SUMMARY.md` +