Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Example environment file for backend
# Application Settings
APP_NAME=InPactAI


# Supabase Configuration
SUPABASE_URL=https://yoursupabaseurl.supabase.co
SUPABASE_KEY=your-supabase-anon-key-here
SUPABASE_SERVICE_KEY=your-service-role-key


# Database Configuration (Supabase PostgreSQL)
# Get this from: Settings β†’ Database β†’ Connection string β†’ URI
DATABASE_URL=postgresql://postgres.your-project-ref:[YOUR-PASSWORD]@aws-0-region.pooler.supabase.com:6543/postgres

# AI Configuration
GROQ_API_KEY=your-groq-api-key
AI_API_KEY=your-openai-api-key-optional

# CORS Origins (comma-separated)
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001

# Server Configuration
HOST=0.0.0.0
PORT=8000
8 changes: 8 additions & 0 deletions backend/SQL
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Table for user profiles
create table if not exists profiles (
id uuid references auth.users(id) on delete cascade,
name text not null,
role text check (role in ('Creator', 'Brand')) not null,
created_at timestamp with time zone default timezone('utc', now()),
primary key (id)
);
145 changes: 145 additions & 0 deletions backend/app/api/routes/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, EmailStr, Field
from gotrue.errors import AuthApiError

from supabase import create_client, Client
from app.core.config import settings

router = APIRouter()
# Use anon key for end-user auth flows and service role for admin ops
supabase_public: Client = create_client(settings.supabase_url, settings.supabase_key)
supabase_admin: Client = create_client(settings.supabase_url, settings.supabase_service_key)


class SignupRequest(BaseModel):
"""
Request schema for user signup.
"""
name: str = Field(..., min_length=2)
email: EmailStr
password: str = Field(..., min_length=8)
role: str = Field(..., pattern="^(Creator|Brand)$")


class SignupResponse(BaseModel):
"""
Response schema for user signup.
"""
message: str
user_id: str | None = None


@router.post("/api/auth/signup", response_model=SignupResponse)
async def signup_user(payload: SignupRequest):

"""
Atomic signup: creates Supabase Auth user and profile row together.
Supabase sends verification email automatically. If profile creation fails, the code attempts to delete the created auth user via supabase_admin (rollback is implemented).
"""
try:
# 1. Create user via Supabase Auth (sends verification email automatically)
try:
auth_resp = supabase_public.auth.sign_up({
"email": payload.email,
"password": payload.password,
})
except AuthApiError as e:
status = 409 if getattr(e, "code", None) == "user_already_exists" else getattr(e, "status", 400) or 400
raise HTTPException(status_code=status, detail=str(e)) from e
user = getattr(auth_resp, "user", None)
if not user or not getattr(user, "id", None):
error_msg = getattr(auth_resp, "error", None)
raise HTTPException(status_code=400, detail=f"Failed to create auth user. {error_msg}")

# 2. Insert profile row
profile = {
"id": user.id,
"name": payload.name,
"role": payload.role
}
res = supabase_admin.table("profiles").insert(profile).execute()
if not res.data:
# 3. Rollback: delete auth user if profile insert fails
rollback_error = None
for attempt in range(2): # try up to 2 times
try:
supabase_admin.auth.admin.delete_user(user.id)
break
except Exception as rollback_err:
rollback_error = rollback_err
if attempt == 0:
continue # retry once
if rollback_error:
# Log the orphaned user for manual cleanup
# logger.error(f"Failed to rollback user {user.id}: {rollback_error}")
raise HTTPException(
status_code=500,
detail=f"Failed to create profile and rollback failed. User {user.id} may be orphaned. Error: {rollback_error}"
) from rollback_error
raise HTTPException(status_code=500, detail="Failed to create profile. User rolled back.")

return SignupResponse(message="Signup successful! Please check your inbox to verify your email.", user_id=user.id)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Signup failed: {str(e)}") from e

# ------------------- LOGIN ROUTE -------------------
class LoginRequest(BaseModel):
"""
Request schema for user login.
"""
email: EmailStr
password: str = Field(..., min_length=8)

class LoginResponse(BaseModel):
"""
Response schema for user login.
"""
message: str
user_id: str | None = None
email: str | None = None
role: str | None = None
name: str | None = None

@router.post("/api/auth/login", response_model=LoginResponse)
async def login_user(payload: LoginRequest):
"""
Login route: authenticates user and enforces email verification.
If email is not verified, returns 403 with a helpful message.
Includes user profile info in response.
"""
try:
# 1. Authenticate user
try:
auth_resp = supabase_public.auth.sign_in_with_password({
"email": payload.email,
"password": payload.password
})
user = getattr(auth_resp, "user", None)
except Exception as e:
# Supabase Python SDK v2 raises exceptions for auth errors
# Import AuthApiError if available
if hasattr(e, "code") and e.code == "email_not_confirmed":
raise HTTPException(status_code=403, detail="Please verify your email before logging in.")
raise HTTPException(status_code=401, detail=str(e))
if not user or not getattr(user, "id", None):
raise HTTPException(status_code=401, detail="Invalid credentials.")

# 2. Fetch user profile
profile_res = supabase_admin.table("profiles").select("id, name, role").eq("id", user.id).single().execute()
profile = profile_res.data if hasattr(profile_res, "data") else None
if not profile:
raise HTTPException(status_code=404, detail="User profile not found.")

return LoginResponse(
message="Login successful.",
user_id=user.id,
email=user.email,
role=profile.get("role"),
name=profile.get("name")
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Login failed: {str(e)}") from e
3 changes: 2 additions & 1 deletion backend/app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
class Settings(BaseSettings):
# Supabase Configuration
supabase_url: str
supabase_key: str # Use the public Anon Key instead of the Service Key
supabase_key: str
supabase_service_key: str

# Database Configuration
database_url: Optional[str] = None
Expand Down
2 changes: 2 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
from app.api.routes import health
from app.services.supabase_client import supabase
from app.api.routes import auth

app = FastAPI(title="Inpact Backend", version="0.1.0")

Expand All @@ -28,6 +29,7 @@
)

app.include_router(health.router)
app.include_router(auth.router)

@app.get("/")
def root():
Expand Down
1 change: 1 addition & 0 deletions backend/env_example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-supabase-anon-key-here
SUPABASE_SERVICE_KEY=your-service-role-key

# Database Configuration (Optional - Supabase PostgreSQL direct connection)

Expand Down
Empty file added frontend/AUTH_README.md
Empty file.
Empty file.
Empty file added frontend/TESTING_GUIDE.md
Empty file.
Empty file added frontend/UI_UX_GUIDE.md
Empty file.
133 changes: 133 additions & 0 deletions frontend/app/brand/home/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"use client";

import AuthGuard from "@/components/auth/AuthGuard";
import { getUserProfile, signOut } from "@/lib/auth-helpers";
import { Briefcase, Loader2, LogOut } from "lucide-react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";

export default function BrandHomePage() {
const router = useRouter();
const [userName, setUserName] = useState<string>("");
const [isLoggingOut, setIsLoggingOut] = useState(false);

useEffect(() => {
async function loadProfile() {
const profile = await getUserProfile();
if (profile) {
setUserName(profile.name);
}
}
loadProfile();
}, []);

const handleLogout = async () => {
try {
setIsLoggingOut(true);
await signOut();
router.push("/login");
} catch (error) {
console.error("Logout error:", error);
setIsLoggingOut(false);
}
};

return (
<AuthGuard requiredRole="Brand">
<div className="min-h-screen bg-linear-to-br from-blue-50 via-white to-purple-50">
{/* Header */}
<header className="border-b border-gray-200 bg-white">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
<div className="flex items-center gap-2">
<Briefcase className="h-6 w-6 text-blue-600" />
<h1 className="bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-2xl font-bold text-transparent">
InPactAI
</h1>
</div>
<button
onClick={handleLogout}
disabled={isLoggingOut}
className="flex items-center gap-2 rounded-lg px-4 py-2 text-sm text-gray-700 transition hover:bg-gray-100 hover:text-gray-900 disabled:opacity-50"
>
{isLoggingOut ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<LogOut className="h-4 w-4" />
)}
{isLoggingOut ? "Logging out..." : "Logout"}
</button>
</div>
</header>

{/* Main Content */}
<main className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<div className="text-center">
{/* Welcome Message */}
<div className="mb-8">
<div className="mb-4 inline-flex h-20 w-20 items-center justify-center rounded-full bg-linear-to-br from-blue-500 to-purple-500">
<Briefcase className="h-10 w-10 text-white" />
</div>
<h2 className="mb-2 text-4xl font-bold text-gray-900">
Hello {userName || "Brand"}!
</h2>
<p className="text-xl text-gray-600">
You are a{" "}
<span className="font-semibold text-blue-600">Brand</span>
</p>
</div>

{/* Info Card */}
<div className="mx-auto max-w-2xl rounded-2xl border border-gray-100 bg-white p-8 shadow-xl">
<div className="space-y-4">
<div className="flex items-start gap-3 text-left">
<div className="mt-2 h-2 w-2 rounded-full bg-blue-600"></div>
<div>
<h3 className="mb-1 font-semibold text-gray-900">
Welcome to InPactAI Brand Dashboard
</h3>
<p className="text-sm text-gray-600">
Discover and collaborate with talented creators to amplify
your brand's message and reach.
</p>
</div>
</div>

<div className="flex items-start gap-3 text-left">
<div className="mt-2 h-2 w-2 rounded-full bg-purple-600"></div>
<div>
<h3 className="mb-1 font-semibold text-gray-900">
Create Campaigns
</h3>
<p className="text-sm text-gray-600">
Launch influencer marketing campaigns, set your budget,
and find the perfect creators for your brand.
</p>
</div>
</div>

<div className="flex items-start gap-3 text-left">
<div className="mt-2 h-2 w-2 rounded-full bg-blue-600"></div>
<div>
<h3 className="mb-1 font-semibold text-gray-900">
Measure Impact
</h3>
<p className="text-sm text-gray-600">
Track campaign performance, ROI, and engagement metrics
with powerful analytics tools.
</p>
</div>
</div>
</div>

<div className="mt-8 border-t border-gray-200 pt-8">
<button className="rounded-lg bg-linear-to-r from-blue-600 to-purple-600 px-8 py-3 font-medium text-white transition-all hover:from-blue-700 hover:to-purple-700">
Create Your First Campaign
</button>
</div>
</div>
</div>
</main>
</div>
</AuthGuard>
);
}
Loading