diff --git a/apps/web/.env.example b/apps/web/.env.example index 3459575..55d2a3f 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,3 +1,9 @@ -# For turbo builds, prefixes with NEXT_PUBLIC_ are automatically considered to force a rebuild -NEXT_PUBLIC_POSTHOG_KEY= -NEXT_PUBLIC_POSTHOG_HOST= \ No newline at end of file +# Supabase Configuration +NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key + +# Database Configuration (Supabase PostgreSQL) +DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres + +# Optional: Supabase Service Role Key (for admin operations) +SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key \ No newline at end of file diff --git a/apps/web/SUPABASE_SETUP.md b/apps/web/SUPABASE_SETUP.md new file mode 100644 index 0000000..1c42318 --- /dev/null +++ b/apps/web/SUPABASE_SETUP.md @@ -0,0 +1,172 @@ +# Supabase Integration Setup + +This guide will help you set up Supabase authentication and database integration for the Tech Companies Portugal application. + +## Prerequisites + +1. A Supabase account (sign up at [supabase.com](https://supabase.com)) +2. Node.js and npm installed + +## Step 1: Create a Supabase Project + +1. Go to [supabase.com](https://supabase.com) and sign in +2. Click "New Project" +3. Choose your organization +4. Enter project details: + - Name: `tech-companies-portugal` (or your preferred name) + - Database Password: Choose a strong password + - Region: Choose the closest region to your users +5. Click "Create new project" + +## Step 2: Get Your Supabase Credentials + +1. In your Supabase dashboard, go to **Settings** → **API** +2. Copy the following values: + - **Project URL** (starts with `https://`) + - **anon public** key (starts with `eyJ`) + - **service_role** key (starts with `eyJ`, keep this secret!) + +## Step 3: Set Up Environment Variables + +1. Copy `.env.example` to `.env.local`: + ```bash + cp .env.example .env.local + ``` + +2. Update `.env.local` with your Supabase credentials: + ```env + NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co + NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key_here + DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.[YOUR-PROJECT-REF].supabase.co:5432/postgres + SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_here + ``` + +## Step 4: Set Up Database Schema + +1. Generate the database migration: + ```bash + npm run db:generate + ``` + +2. Apply the migration to your Supabase database: + ```bash + npm run db:migrate + ``` + +## Step 5: Configure Supabase Authentication + +1. In your Supabase dashboard, go to **Authentication** → **Settings** +2. Configure the following: + + **Site URL**: `http://localhost:3000` (for development) + + **Redirect URLs**: Add these URLs: + - `http://localhost:3000/auth/callback` + - `http://localhost:3000/api/auth/callback` + - `https://your-domain.com/auth/callback` (for production) + - `https://your-domain.com/api/auth/callback` (for production) + +3. **Email Templates** (optional): + - Customize the confirmation and reset password email templates + - Update the sender email address + +## Step 6: Enable Email Authentication + +1. In **Authentication** → **Providers** +2. Make sure **Email** is enabled +3. Configure email settings: + - **Enable email confirmations**: ✅ (recommended) + - **Enable secure email change**: ✅ (recommended) + - **Enable double confirm changes**: ✅ (recommended) + +## Step 7: Test the Integration + +1. Start the development server: + ```bash + npm run dev + ``` + +2. Navigate to `http://localhost:3000/auth` +3. Try signing up with a new account +4. Check your email for the confirmation link +5. Sign in with your credentials + +## Database Schema + +The application uses the following database schema: + +### `user_profiles` table +- `id` (UUID, Primary Key): User ID from Supabase Auth +- `email` (Text): User's email address +- `full_name` (Text): User's full name +- `avatar_url` (Text): URL to user's avatar image +- `is_active` (Boolean): Whether the user account is active +- `created_at` (Timestamp): When the profile was created +- `updated_at` (Timestamp): When the profile was last updated + +## Features Implemented + +### Authentication +- ✅ Email/password sign up and sign in +- ✅ Email confirmation +- ✅ Password reset +- ✅ Sign out functionality +- ✅ Protected routes (configurable) +- ✅ User session management + +### User Interface +- ✅ Sign in/Sign up form with validation +- ✅ Loading states and error handling +- ✅ Success messages +- ✅ Responsive design +- ✅ Consistent styling with existing design system + +### Database Integration +- ✅ User profile creation on sign up +- ✅ Drizzle ORM integration +- ✅ Database migrations +- ✅ Type-safe database operations + +## Security Considerations + +1. **Environment Variables**: Never commit `.env.local` to version control +2. **Service Role Key**: Keep the service role key secret and only use it server-side +3. **CORS**: Configure CORS settings in Supabase if needed +4. **Rate Limiting**: Consider implementing rate limiting for auth endpoints +5. **Email Verification**: Enable email confirmation for production + +## Production Deployment + +1. Update environment variables with production URLs +2. Set up proper redirect URLs in Supabase +3. Configure email templates for your domain +4. Set up monitoring and logging +5. Consider implementing additional security measures + +## Troubleshooting + +### Common Issues + +1. **"Invalid API key" error**: + - Check that your environment variables are correctly set + - Verify the API keys in your Supabase dashboard + +2. **"Redirect URL not allowed" error**: + - Add your redirect URLs to the Supabase authentication settings + - Make sure the URLs match exactly (including protocol and port) + +3. **Database connection errors**: + - Verify your `DATABASE_URL` is correct + - Check that your database password is properly URL-encoded + - Ensure your IP is allowed in Supabase database settings + +4. **Email not sending**: + - Check Supabase email settings + - Verify your email templates are configured + - Check spam folder for confirmation emails + +### Getting Help + +- [Supabase Documentation](https://supabase.com/docs) +- [Supabase Discord](https://discord.supabase.com) +- [Drizzle Documentation](https://orm.drizzle.team) \ No newline at end of file diff --git a/apps/web/drizzle.config.ts b/apps/web/drizzle.config.ts new file mode 100644 index 0000000..36b244c --- /dev/null +++ b/apps/web/drizzle.config.ts @@ -0,0 +1,10 @@ +import type { Config } from 'drizzle-kit' + +export default { + schema: './src/lib/db/schema.ts', + out: './src/lib/db/migrations', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +} satisfies Config \ No newline at end of file diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts new file mode 100644 index 0000000..dd740b2 --- /dev/null +++ b/apps/web/middleware.ts @@ -0,0 +1,20 @@ +import { updateSession } from '@/lib/supabase/middleware' +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' + +export async function middleware(request: NextRequest) { + return await updateSession(request) +} + +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) + * Feel free to modify this pattern to include more paths. + */ + '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + ], +} \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 1db186d..d7e9f6e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -12,23 +12,31 @@ "clean": "git clean -xdf .next .turbo node_modules playwright-report test-results", "test": "echo 'No tests to run'", "test:e2e": "start-server-and-test start http://localhost:3000 'playwright test'", - "test:e2e:ui": "start-server-and-test start http://localhost:3000 'playwright test --ui'" + "test:e2e:ui": "start-server-and-test start http://localhost:3000 'playwright test --ui'", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:studio": "drizzle-kit studio" }, "dependencies": { "@radix-ui/react-collapsible": "1.1.2", "@radix-ui/react-label": "2.1.1", "@radix-ui/react-select": "2.1.4", "@radix-ui/react-slot": "1.1.1", + "@supabase/ssr": "^0.6.1", + "@supabase/supabase-js": "^2.51.0", "@tech-companies-portugal/analytics": "*", + "@types/pg": "^8.15.4", "cheerio": "1.0.0-rc.12", "class-variance-authority": "0.7.0", "clsx": "2.1.0", "date-fns": "3.3.1", + "drizzle-orm": "^0.44.3", "geist": "1.3.1", "lucide-react": "0.469.0", "motion": "11.17.0", "next": "15.2.3", "nuqs": "2.3.1", + "postgres": "^3.4.7", "react": "19.0.0", "react-countup": "6.5.3", "react-dom": "19.0.0", @@ -47,6 +55,7 @@ "@types/react": "19.0.2", "@types/react-dom": "19.0.2", "autoprefixer": "^10.0.1", + "drizzle-kit": "^0.31.4", "postcss": "^8", "start-server-and-test": "2.0.10", "tailwindcss": "^3.3.0", diff --git a/apps/web/src/app/api/auth/callback/route.ts b/apps/web/src/app/api/auth/callback/route.ts new file mode 100644 index 0000000..621e307 --- /dev/null +++ b/apps/web/src/app/api/auth/callback/route.ts @@ -0,0 +1,26 @@ +import { createClient } from '@/lib/supabase/server' +import { createUserProfile } from '@/lib/auth' +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') ?? '/' + + if (code) { + const supabase = createClient() + const { data, error } = await supabase.auth.exchangeCodeForSession(code) + + if (!error && data.user) { + // Create user profile if it doesn't exist + await createUserProfile( + data.user.id, + data.user.email!, + data.user.user_metadata?.full_name + ) + } + } + + // URL to redirect to after sign in process completes + return NextResponse.redirect(`${origin}${next}`) +} \ No newline at end of file diff --git a/apps/web/src/app/auth/README.md b/apps/web/src/app/auth/README.md new file mode 100644 index 0000000..b64b1f5 --- /dev/null +++ b/apps/web/src/app/auth/README.md @@ -0,0 +1,126 @@ +# Authentication Page + +This directory contains the authentication functionality for the Tech Companies Portugal application, integrated with Supabase Auth and PostgreSQL database. + +## Features + +- **Supabase Integration**: Full authentication with Supabase Auth +- **Sign In/Sign Up Toggle**: Users can switch between sign in and sign up modes +- **Email Confirmation**: Automatic email verification for new accounts +- **Password Reset**: Forgot password functionality with email reset +- **Form Validation**: Real-time validation with error messages +- **Loading States**: Visual feedback during form submission +- **User Session Management**: Automatic session handling and persistence +- **Responsive Design**: Works on all device sizes +- **Consistent Styling**: Uses the existing design system components + +## Authentication Flow + +### Sign Up Process +1. User fills out sign up form (email, password, full name) +2. Form validation checks all required fields +3. Supabase creates user account +4. User receives confirmation email +5. User clicks confirmation link +6. User profile is created in PostgreSQL database +7. User is redirected to home page + +### Sign In Process +1. User enters email and password +2. Supabase validates credentials +3. User session is created and stored +4. User is redirected to home page +5. Navbar updates to show user email and sign out button + +### Password Reset Process +1. User clicks "Forgot your password?" +2. User enters email address +3. Supabase sends password reset email +4. User clicks reset link in email +5. User sets new password +6. User can sign in with new password + +## Components Used + +- `RetroContainer`: Main container with retro styling +- `Card`: Form container with header and content sections +- `Input`: Form input fields with error states +- `Button`: Submit button with loading state +- `Label`: Form field labels +- `useAuth`: Custom hook for authentication state management + +## Design Patterns + +The authentication page follows the existing design patterns from the codebase: + +- **Retro Container**: Uses the `RetroContainer` component with default variant for the main form container +- **Typography**: Uses `font-mono` for consistent monospace font styling +- **Color Scheme**: Uses the existing CSS custom properties for colors +- **Spacing**: Follows the established spacing patterns with Tailwind classes +- **Interactive States**: Hover effects and focus states match the existing components + +## Form Validation + +- Email validation with regex pattern +- Password minimum length (6 characters) +- Password confirmation matching for sign up +- Required field validation +- Real-time error clearing when user starts typing +- Supabase error message display + +## Database Integration + +### User Profiles Table +- `id`: UUID (matches Supabase Auth user ID) +- `email`: User's email address +- `full_name`: User's full name +- `avatar_url`: Optional avatar image URL +- `is_active`: Account status +- `created_at`: Profile creation timestamp +- `updated_at`: Last update timestamp + +### Automatic Profile Creation +When a user signs up and confirms their email, a profile is automatically created in the PostgreSQL database using Drizzle ORM. + +## Navigation + +- "Sign In" button in navbar (when not authenticated) +- User email display and "Sign Out" button in navbar (when authenticated) +- "Back to Home" button on the auth page +- Toggle between sign in and sign up modes + +## Security Features + +- Email confirmation required for new accounts +- Secure password reset via email +- Session management with automatic cleanup +- Protected routes (configurable) +- Environment variable protection +- Type-safe database operations + +## API Routes + +- `/api/auth/callback`: Handles Supabase auth callbacks and profile creation +- Middleware: Manages authentication state and protected routes + +## Environment Variables Required + +```env +NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +DATABASE_URL=your_supabase_postgresql_connection_string +SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key +``` + +## Setup Instructions + +See `SUPABASE_SETUP.md` in the project root for detailed setup instructions. + +## Future Enhancements + +- Social login providers (Google, GitHub, etc.) +- Two-factor authentication +- User profile management page +- Admin dashboard for user management +- Advanced role-based access control +- Audit logging for security events \ No newline at end of file diff --git a/apps/web/src/app/auth/layout.tsx b/apps/web/src/app/auth/layout.tsx new file mode 100644 index 0000000..272a209 --- /dev/null +++ b/apps/web/src/app/auth/layout.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from "next/types"; + +export const metadata: Metadata = { + title: "Authentication - Tech Companies Portugal", + description: "Sign in or sign up to access your account", +}; + +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + return children; +} \ No newline at end of file diff --git a/apps/web/src/app/auth/page.tsx b/apps/web/src/app/auth/page.tsx new file mode 100644 index 0000000..2aec5f9 --- /dev/null +++ b/apps/web/src/app/auth/page.tsx @@ -0,0 +1,284 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { RetroContainer } from "@/components/ui/retro-container"; +import { useAuth } from "@/components/hooks/useAuth"; + +export default function AuthPage() { + const { signIn, signUp, resetPassword } = useAuth(); + const [isSignIn, setIsSignIn] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const [errors, setErrors] = useState>({}); + const [successMessage, setSuccessMessage] = useState(""); + const [formData, setFormData] = useState({ + email: "", + password: "", + confirmPassword: "", + name: "", + }); + + const validateForm = () => { + const newErrors: Record = {}; + + if (!formData.email) { + newErrors.email = "Email is required"; + } else if (!/\S+@\S+\.\S+/.test(formData.email)) { + newErrors.email = "Please enter a valid email"; + } + + if (!formData.password) { + newErrors.password = "Password is required"; + } else if (formData.password.length < 6) { + newErrors.password = "Password must be at least 6 characters"; + } + + if (!isSignIn) { + if (!formData.name) { + newErrors.name = "Name is required"; + } + + if (!formData.confirmPassword) { + newErrors.confirmPassword = "Please confirm your password"; + } else if (formData.password !== formData.confirmPassword) { + newErrors.confirmPassword = "Passwords do not match"; + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + setIsLoading(true); + setErrors({}); + setSuccessMessage(""); + + try { + if (isSignIn) { + const { error } = await signIn(formData.email, formData.password); + if (error) { + setErrors({ general: error.message }); + } + } else { + const { error } = await signUp(formData.email, formData.password, formData.name); + if (error) { + setErrors({ general: error.message }); + } else { + setSuccessMessage("Check your email for a confirmation link!"); + } + } + } catch (error) { + console.error("Authentication failed:", error); + setErrors({ general: "An unexpected error occurred. Please try again." }); + } finally { + setIsLoading(false); + } + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + + // Clear error when user starts typing + if (errors[name]) { + setErrors(prev => ({ ...prev, [name]: "" })); + } + if (errors.general) { + setErrors(prev => ({ ...prev, general: "" })); + } + if (successMessage) { + setSuccessMessage(""); + } + }; + + return ( +
+
+ +
+ + + + + {isSignIn ? "Sign In" : "Sign Up"} + + + {isSignIn + ? "Welcome back! Sign in to your account" + : "Create a new account to get started" + } + + + + + {errors.general && ( +
+ {errors.general} +
+ )} + {successMessage && ( +
+ {successMessage} +
+ )} +
+ {!isSignIn && ( +
+ + + {errors.name && ( +

{errors.name}

+ )} +
+ )} + +
+ + + {errors.email && ( +

{errors.email}

+ )} +
+ +
+ + + {errors.password && ( +

{errors.password}

+ )} +
+ + {!isSignIn && ( +
+ + + {errors.confirmPassword && ( +

{errors.confirmPassword}

+ )} +
+ )} + + +
+ +
+

+ {isSignIn ? "Don't have an account? " : "Already have an account? "} + +

+
+ + {isSignIn && ( +
+ +
+ )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/components/Navbar.tsx b/apps/web/src/components/Navbar.tsx index 1651c5f..851ed47 100644 --- a/apps/web/src/components/Navbar.tsx +++ b/apps/web/src/components/Navbar.tsx @@ -6,8 +6,11 @@ import logo from "../../public/assets/images/logo.png"; import ExploreButton from "./ExploreButton"; import FiltersPanelButton from "./FiltersPanelButton"; import { Button } from "./ui/button"; +import { useAuth } from "./hooks/useAuth"; export default function Navbar() { + const { user, signOut, loading } = useAuth(); + return (
+ {!loading && ( + <> + {user ? ( +
+ + {user.email} + + +
+ ) : ( + + )} + + )}