Skip to content
Open
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
12 changes: 8 additions & 4 deletions backend/app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Configuration settings for FastAPI app
from pydantic_settings import BaseSettings
from typing import Optional
from typing import Optional, Dict, Any
import json

class Settings(BaseSettings):
# Supabase Configuration
Expand All @@ -26,11 +26,15 @@ class Settings(BaseSettings):
# Application Settings
app_name: Optional[str] = None

# JWT Authentication
SUPABASE_JWT_SECRET: str # JWT Secret from Supabase Dashboard → Settings → API → JWT Settings
# JWT Authentication (RAW JSON STRING)
SUPABASE_JWT_PUBLIC_KEY: str

model_config = {
"env_file": ".env"
}

@property
def supabase_jwt_jwk(self) -> Dict[str, Any]:
return json.loads(self.SUPABASE_JWT_PUBLIC_KEY)

settings = Settings()
1 change: 0 additions & 1 deletion backend/app/core/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,3 @@ async def route(user = Depends(get_optional_user)):
return await get_current_user(credentials)
except HTTPException:
return None

124 changes: 23 additions & 101 deletions backend/app/core/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,153 +3,76 @@
Handles token validation, creation, and user verification
"""

import jwt
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
from jose import jwt, JWTError
from fastapi import HTTPException, status
from typing import Optional, Dict, Any
from app.core.config import settings


class JWTHandler:
"""Handle all JWT operations with Supabase tokens"""
"""Handle Supabase JWT verification only"""

def __init__(self):
try:
self.secret_key = settings.SUPABASE_JWT_SECRET
if not self.secret_key:
raise ValueError("SUPABASE_JWT_SECRET is not set in environment variables")
except Exception as e:
raise ValueError(f"Failed to load JWT secret: {str(e)}. Please set SUPABASE_JWT_SECRET in your .env file.")
self.algorithm = "HS256" # Supabase uses HS256 for legacy keys
self.public_key = settings.SUPABASE_JWT_PUBLIC_KEY
if not self.public_key:
raise ValueError("SUPABASE_JWT_PUBLIC_KEY is not set")

def decode_token(self, token: str) -> Dict[str, Any]:
"""
Decode and validate JWT token from Supabase
self.algorithm = "ES256"

Args:
token: JWT token string (without 'Bearer ' prefix)

Returns:
Decoded payload with user information

Raises:
HTTPException: If token is invalid or expired
"""
def decode_token(self, token: str) -> Dict[str, Any]:
try:
# Decode JWT using Supabase secret
# Note: We don't verify audience as Supabase tokens may have different audience claims
# The signature verification is sufficient for security
payload = jwt.decode(
token,
self.secret_key,
self.public_key, # ✅ PUBLIC KEY
algorithms=[self.algorithm],
options={
"verify_signature": True,
"verify_exp": True,
"verify_iat": True,
"verify_aud": False # Disable audience verification for Supabase tokens
}
audience="authenticated", # Supabase default
)

# Validate required fields
if 'sub' not in payload:
if "sub" not in payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token: missing user ID"
detail="Invalid token: missing user id",
)

return payload

except jwt.ExpiredSignatureError:
except JWTError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired"
)

except jwt.InvalidTokenError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Invalid token: {str(e)}"
detail=f"Invalid token: {str(e)}",
)

def verify_token(self, token: str) -> bool:
"""
Quick token validation without full decode

Args:
token: JWT token string

Returns:
True if valid, False otherwise
"""
try:
jwt.decode(
token,
self.secret_key,
self.public_key,
algorithms=[self.algorithm],
options={
"verify_signature": True,
"verify_exp": True,
"verify_aud": False # Disable audience verification
}
audience="authenticated",
)
return True
except:
except JWTError:
return False

def get_user_id_from_token(self, token: str) -> str:
"""
Extract user ID from token

Args:
token: JWT token string

Returns:
User UUID as string
"""
payload = self.decode_token(token)
return payload['sub']
return self.decode_token(token)["sub"]

def get_user_email_from_token(self, token: str) -> Optional[str]:
"""
Extract user email from token

Args:
token: JWT token string

Returns:
User email if present in token
"""
payload = self.decode_token(token)
return payload.get('email')
return self.decode_token(token).get("email")

def get_user_role_from_token(self, token: str) -> Optional[str]:
"""
Extract user role from token metadata

Args:
token: JWT token string

Returns:
User role (creator/brand) if present
"""
payload = self.decode_token(token)

# Supabase stores custom claims in user_metadata or app_metadata
user_metadata = payload.get('user_metadata', {})
app_metadata = payload.get('app_metadata', {})

return (
user_metadata.get('role') or
app_metadata.get('role') or
payload.get('role')
payload.get("user_metadata", {}).get("role")
or payload.get("app_metadata", {}).get("role")
or payload.get("role")
)


# Singleton instance
jwt_handler = JWTHandler()



# Legacy functions for backward compatibility (optional - requires passlib)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify a password against a hash (if needed for custom auth)"""
Expand All @@ -169,4 +92,3 @@ def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
except ImportError:
raise ImportError("passlib is required for password hashing. Install with: pip install passlib[bcrypt]")

54 changes: 24 additions & 30 deletions backend/app/main.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,36 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import os
from app.api.routes import health
from app.core.supabase_clients import supabase_anon as supabase
from app.api.routes import auth
from app.api.routes import gemini_generate
from app.api.routes import campaigns
from app.api.routes import groq_generate
from app.api.routes import collaborations
from app.api.routes import creators
from app.api.routes import proposals
from app.api.routes import analytics
from app.api.routes import ai_analytics
from app.api.routes import profiles
app = FastAPI(title="Inpact Backend", version="0.1.0")

# Verify Supabase client initialization on startup
try:
# Try a lightweight query
response = supabase.table("_supabase_test").select("*").limit(1).execute()
print("✅ Supabase client initialized successfully.")
except Exception as e:
error_msg = str(e)
if "Could not find the table" in error_msg:
print("⚠️ Supabase client connected, but test table does not exist. Connection is working.")
else:
print(f"❌ Failed to verify Supabase connection: {e}")
from app.api.routes import (
health,
auth,
gemini_generate,
campaigns,
groq_generate,
collaborations,
creators,
proposals,
analytics,
ai_analytics,
profiles,
)

# --- CORS Setup ---
app = FastAPI(title="Inpact Backend", version="0.1.0")

# 🔥 FORCE OPEN CORS (DEBUG MODE)
app.add_middleware(
CORSMiddleware,
allow_origins=os.getenv("ALLOWED_ORIGINS", "http://localhost:3000").split(","),
allow_credentials=True,
allow_origins=["*"],
allow_credentials=False, # must be False when using "*"
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(gemini_generate.router)


# Routes
app.include_router(health.router)
app.include_router(auth.router)
app.include_router(gemini_generate.router)
app.include_router(campaigns.router)
app.include_router(groq_generate.router)
app.include_router(collaborations.router)
Expand All @@ -49,4 +42,5 @@

@app.get("/")
def root():
return {"message": "Welcome to Inpact Backend 🚀"}
return {"message": "Inpact Backend Running 🚀"}

2 changes: 1 addition & 1 deletion backend/env_example
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3001
# Location: Dashboard → Project Settings → API → JWT Settings → JWT Secret
# Use the JWT Secret (NOT the anon key!)

SUPABASE_JWT_SECRET=your-jwt-secret-from-supabase-dashboard
SUPABASE_JWT_SECRET=your_jwt_key
8 changes: 6 additions & 2 deletions backend/test_jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import requests
import os
from dotenv import load_dotenv
from jose import jwt, JWTError

load_dotenv()

Expand All @@ -15,8 +16,10 @@
SUPABASE_ANON_KEY = os.getenv("SUPABASE_KEY")

# Test user credentials (update with your test account)
TEST_EMAIL = "test@example.com"
TEST_PASSWORD = "Test123!@#"
TEST_EMAIL = "anu906162@gmail.com"
TEST_PASSWORD = "@rani00@"
# @rani00@



def get_jwt_token():
Expand Down Expand Up @@ -107,3 +110,4 @@ def test_invalid_token():

print("=== Tests Complete ===")


12 changes: 6 additions & 6 deletions frontend/app/creator/campaign-wall/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,14 @@ export default function CampaignWallPage() {
<div className="mb-6 flex flex-col flex-wrap gap-4 rounded-xl bg-white p-6 shadow-md sm:flex-row">
<div className="min-w-[200px] flex-1">
<div className="relative">
<Search className="absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 text-gray-400" />
<Search className="absolute top-1/2 left-3 h-5 w-5 -translate-y-1/2 text-black" />
<input
type="text"
placeholder="Search campaigns..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && handleSearch()}
className="w-full rounded-lg border border-gray-300 py-3 pr-4 pl-10 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
className="w-full rounded-lg border text-black border-gray-300 py-3 pr-4 pl-10 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
/>
</div>
</div>
Expand All @@ -294,7 +294,7 @@ export default function CampaignWallPage() {
<select
value={platformFilter}
onChange={(e) => setPlatformFilter(e.target.value)}
className="min-w-[120px] appearance-none rounded-lg border border-gray-300 py-3 pr-10 pl-10 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
className="min-w-[120px] appearance-none text-gray-500 rounded-lg border border-gray-300 py-3 pr-10 pl-10 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
>
<option value="">All Platforms</option>
{PLATFORM_OPTIONS.map((option) => (
Expand All @@ -307,7 +307,7 @@ export default function CampaignWallPage() {
<select
value={nicheFilter}
onChange={(e) => setNicheFilter(e.target.value)}
className="min-w-[120px] appearance-none rounded-lg border border-gray-300 py-3 pr-10 pl-4 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
className="min-w-[120px] appearance-none rounded-lg border text-gray-500 border-gray-300 py-3 pr-10 pl-4 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
>
<option value="">All Niches</option>
{NICHE_OPTIONS.map((option) => (
Expand All @@ -321,15 +321,15 @@ export default function CampaignWallPage() {
placeholder="Min Budget"
value={budgetMin}
onChange={(e) => setBudgetMin(e.target.value)}
className="w-28 rounded-lg border border-gray-300 px-3 py-3 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
className="w-34 rounded-lg border text-black border-gray-300 px-3 py-3 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
min={0}
/>
<input
type="number"
placeholder="Max Budget"
value={budgetMax}
onChange={(e) => setBudgetMax(e.target.value)}
className="w-28 rounded-lg border border-gray-300 px-3 py-3 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
className="w-34 rounded-lg border text-black border-gray-300 px-3 py-3 focus:border-purple-500 focus:ring-2 focus:ring-purple-200 focus:outline-none"
min={0}
/>
<button
Expand Down
Loading