From a688a455ad8cf4c20c96f0c12efaa365b76e8549 Mon Sep 17 00:00:00 2001 From: Saahi30 Date: Mon, 10 Nov 2025 20:59:47 +0530 Subject: [PATCH] Refactor frontend auth helpers and supabase client --- frontend/lib/api/collaborations.ts | 77 +++++++++++++ frontend/lib/api/creators.ts | 167 +++++++++++++++++++++++++++++ frontend/lib/auth-helpers.ts | 106 +++++++++++++++--- frontend/lib/supabaseClient.ts | 38 ++++++- 4 files changed, 374 insertions(+), 14 deletions(-) create mode 100644 frontend/lib/api/collaborations.ts create mode 100644 frontend/lib/api/creators.ts diff --git a/frontend/lib/api/collaborations.ts b/frontend/lib/api/collaborations.ts new file mode 100644 index 0000000..110d1ef --- /dev/null +++ b/frontend/lib/api/collaborations.ts @@ -0,0 +1,77 @@ +import { authenticatedFetch } from "../auth-helpers"; + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; + +export interface CollaborationIdea { + title: string; + description: string; + collaboration_type: string; + why_it_works: string; +} + +export interface CollaborationIdeasResponse { + ideas: CollaborationIdea[]; +} + +/** + * Generate collaboration ideas between the current creator and a target creator + */ +export async function generateCollaborationIdeas( + targetCreatorId: string +): Promise { + const url = `${API_BASE_URL}/collaborations/generate-ideas`; + const response = await authenticatedFetch(url, { + method: "POST", + body: JSON.stringify({ target_creator_id: targetCreatorId }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.detail || `Failed to generate collaboration ideas: ${response.statusText}` + ); + } + + return response.json(); +} + +export interface CreatorRecommendationForIdea { + creator_id: string; + display_name: string; + profile_picture_url: string | null; + primary_niche: string; + match_score: number; + reasoning: string; +} + +export interface RecommendCreatorResponse { + recommended_creator: CreatorRecommendationForIdea; + alternatives: CreatorRecommendationForIdea[]; +} + +/** + * Recommend the best creator from a list of candidates for a collaboration idea + */ +export async function recommendCreatorForIdea( + collaborationIdea: string, + candidateCreatorIds: string[] +): Promise { + const url = `${API_BASE_URL}/collaborations/recommend-creator`; + const response = await authenticatedFetch(url, { + method: "POST", + body: JSON.stringify({ + collaboration_idea: collaborationIdea, + candidate_creator_ids: candidateCreatorIds, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error( + errorData.detail || `Failed to get recommendation: ${response.statusText}` + ); + } + + return response.json(); +} + diff --git a/frontend/lib/api/creators.ts b/frontend/lib/api/creators.ts new file mode 100644 index 0000000..829f1ed --- /dev/null +++ b/frontend/lib/api/creators.ts @@ -0,0 +1,167 @@ +import { authenticatedFetch } from "../auth-helpers"; + +const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; + +export interface CreatorBasic { + id: string; + display_name: string; + tagline: string | null; + bio: string | null; + profile_picture_url: string | null; + primary_niche: string; + secondary_niches: string[] | null; + total_followers: number; + engagement_rate: number | null; + is_verified_creator: boolean; + profile_completion_percentage: number; +} + +export interface CreatorFull extends CreatorBasic { + user_id: string; + cover_image_url: string | null; + website_url: string | null; + youtube_url: string | null; + youtube_handle: string | null; + youtube_subscribers: number | null; + instagram_url: string | null; + instagram_handle: string | null; + instagram_followers: number | null; + tiktok_url: string | null; + tiktok_handle: string | null; + tiktok_followers: number | null; + twitter_url: string | null; + twitter_handle: string | null; + twitter_followers: number | null; + twitch_url: string | null; + twitch_handle: string | null; + twitch_followers: number | null; + linkedin_url: string | null; + facebook_url: string | null; + content_types: string[] | null; + content_language: string[] | null; + total_reach: number | null; + average_views: number | null; + audience_age_primary: string | null; + audience_gender_split: Record | null; + audience_locations: Record | null; + audience_interests: string[] | null; + average_engagement_per_post: number | null; + posting_frequency: string | null; + best_performing_content_type: string | null; + years_of_experience: number | null; + content_creation_full_time: boolean; + team_size: number; + equipment_quality: string | null; + editing_software: string[] | null; + collaboration_types: string[] | null; + preferred_brands_style: string[] | null; + rate_per_post: number | null; + rate_per_video: number | null; + rate_per_story: number | null; + rate_per_reel: number | null; + rate_negotiable: boolean; + accepts_product_only_deals: boolean; + minimum_deal_value: number | null; + preferred_payment_terms: string | null; + portfolio_links: string[] | null; + past_brand_collaborations: string[] | null; + case_study_links: string[] | null; + media_kit_url: string | null; + created_at: string | null; + last_active_at: string | null; +} + +export interface ListCreatorsParams { + search?: string; + niche?: string; + limit?: number; + offset?: number; +} + +/** + * List all creators with optional search and filters + */ +export async function listCreators( + params: ListCreatorsParams = {} +): Promise { + const queryParams = new URLSearchParams(); + if (params.search) queryParams.append("search", params.search); + if (params.niche) queryParams.append("niche", params.niche); + if (params.limit) queryParams.append("limit", params.limit.toString()); + if (params.offset) queryParams.append("offset", params.offset.toString()); + + const url = `${API_BASE_URL}/creators?${queryParams.toString()}`; + const response = await authenticatedFetch(url); + + if (!response.ok) { + // Try to get error details from response + let errorMessage = `Failed to fetch creators: ${response.statusText}`; + try { + const errorData = await response.json(); + if (errorData.detail) { + errorMessage = errorData.detail; + } + } catch { + // If response is not JSON, use status text + } + + const error = new Error(errorMessage); + (error as any).status = response.status; + throw error; + } + + return response.json(); +} + +/** + * Get full details of a specific creator + */ +export async function getCreatorDetails(creatorId: string): Promise { + const url = `${API_BASE_URL}/creators/${creatorId}`; + const response = await authenticatedFetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch creator details: ${response.statusText}`); + } + + return response.json(); +} + +export interface CreatorRecommendation { + id: string; + display_name: string; + profile_picture_url: string | null; + primary_niche: string | null; + total_followers: number | null; + engagement_rate: number | null; + top_platforms?: string[] | null; + match_score: number; + reason: string; +} + +export async function getCreatorRecommendations(limit = 4): Promise { + const url = `${API_BASE_URL}/creators/recommendations?limit=${limit}`; + const response = await authenticatedFetch(url); + + if (!response.ok) { + const errText = await response.text().catch(() => ""); + throw new Error(`Failed to fetch recommendations: ${response.status} ${errText}`); + } + return response.json(); +} + +/** + * Get list of all unique niches + */ +export async function listNiches(): Promise { + const url = `${API_BASE_URL}/creators/niches/list`; + const response = await authenticatedFetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch niches: ${response.statusText}`); + } + + const data = await response.json(); + return data.niches || []; +} + diff --git a/frontend/lib/auth-helpers.ts b/frontend/lib/auth-helpers.ts index 627f2c4..1c729e4 100644 --- a/frontend/lib/auth-helpers.ts +++ b/frontend/lib/auth-helpers.ts @@ -1,4 +1,4 @@ -import { supabase } from "./supabaseClient"; +import { getAuthToken, supabase } from "./supabaseClient"; export interface UserProfile { id: string; @@ -16,7 +16,9 @@ export async function getCurrentUser() { data: { user }, error, } = await supabase.auth.getUser(); - if (error) throw error; + if (error || !user) { + return null; + } return user; } @@ -34,7 +36,10 @@ export async function getUserProfile(): Promise { .eq("id", user.id) .single(); - if (error) throw error; + if (error) { + console.error("Error fetching profile:", error); + return null; + } return data as UserProfile; } catch (error) { console.error("Error fetching user profile:", error); @@ -53,16 +58,9 @@ export async function signOut() { /** * Check if user has a specific role */ -export async function checkUserRole( - requiredRole: "Creator" | "Brand" -): Promise { - try { - const profile = await getUserProfile(); - return profile?.role === requiredRole; - } catch (error) { - console.error("Error checking user role:", error); - return false; - } +export async function checkUserRole(): Promise<"Creator" | "Brand" | null> { + const profile = await getUserProfile(); + return profile?.role || null; } /** @@ -78,6 +76,88 @@ export async function hasCompletedOnboarding(): Promise { } } +/** + * Token refresh handling (automatic with Supabase) + */ +export async function ensureValidToken() { + const { data: { session } } = await supabase.auth.getSession(); + + if (!session) { + throw new Error("No active session"); + } + + // Supabase automatically refreshes if needed + return session.access_token; +} + +/** + * Make authenticated API calls to backend + */ +export async function authenticatedFetch(url: string, options: RequestInit = {}) { + // Try to get token, refresh if needed + let token = await getAuthToken(); + + // If no token, try to refresh session + if (!token) { + try { + const { data: { session }, error } = await supabase.auth.refreshSession(); + if (error) { + console.error("Failed to refresh session:", error); + // Don't throw here - let the 401 response handle it + token = undefined; + } else { + token = session?.access_token || undefined; + } + } catch (err) { + console.error("Error refreshing token:", err); + token = undefined; + } + } + + if (!token) { + // Return a response that will trigger 401, don't throw + // This allows the calling code to handle it gracefully + return new Response( + JSON.stringify({ error: "No authentication token available" }), + { status: 401, headers: { "Content-Type": "application/json" } } + ); + } + + const headers = { + ...options.headers, + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json", + }; + + const response = await fetch(url, { + ...options, + headers, + }); + + // If 401, try refreshing token once + if (response.status === 401) { + try { + const { data: { session }, error } = await supabase.auth.refreshSession(); + if (!error && session?.access_token) { + // Retry with new token + const retryHeaders = { + ...options.headers, + "Authorization": `Bearer ${session.access_token}`, + "Content-Type": "application/json", + }; + return fetch(url, { + ...options, + headers: retryHeaders, + }); + } + } catch (err) { + console.error("Error refreshing token on 401:", err); + } + } + + return response; +} + /** * Map Supabase auth errors to user-friendly messages */ diff --git a/frontend/lib/supabaseClient.ts b/frontend/lib/supabaseClient.ts index 5951780..c7dfe70 100644 --- a/frontend/lib/supabaseClient.ts +++ b/frontend/lib/supabaseClient.ts @@ -9,4 +9,40 @@ if (!supabaseUrl || !supabaseAnonKey) { ); } -export const supabase = createClient(supabaseUrl, supabaseAnonKey); +export const supabase = createClient(supabaseUrl, supabaseAnonKey, { + auth: { + autoRefreshToken: true, // Auto-refresh JWT + persistSession: true, // Persist session in localStorage + detectSessionInUrl: true, // Handle OAuth redirects + }, +}); + +// Helper to get current JWT token +export const getAuthToken = async () => { + const { data: { session } } = await supabase.auth.getSession(); + return session?.access_token; +}; + +// Helper to refresh token +export const refreshAuthToken = async () => { + const { data, error } = await supabase.auth.refreshSession(); + if (error) throw error; + return data.session?.access_token; +}; + +export const getCurrentUser = async () => { + const { data: { user }, error } = await supabase.auth.getUser(); + if (error) { + console.error("Error getting user:", error); + return null; + } + return user; +}; + +export const signOut = async () => { + const { error } = await supabase.auth.signOut(); + if (error) { + console.error("Error signing out:", error); + throw error; + } +};