Skip to content

Commit c715ff4

Browse files
authored
Merge pull request #152 from AOSSIE-Org/feat/frontend-auth-supabase
feat(auth): refactor frontend auth helpers and supabase client
2 parents 75d828b + a688a45 commit c715ff4

File tree

4 files changed

+374
-14
lines changed

4 files changed

+374
-14
lines changed

frontend/lib/api/collaborations.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { authenticatedFetch } from "../auth-helpers";
2+
3+
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
4+
5+
export interface CollaborationIdea {
6+
title: string;
7+
description: string;
8+
collaboration_type: string;
9+
why_it_works: string;
10+
}
11+
12+
export interface CollaborationIdeasResponse {
13+
ideas: CollaborationIdea[];
14+
}
15+
16+
/**
17+
* Generate collaboration ideas between the current creator and a target creator
18+
*/
19+
export async function generateCollaborationIdeas(
20+
targetCreatorId: string
21+
): Promise<CollaborationIdeasResponse> {
22+
const url = `${API_BASE_URL}/collaborations/generate-ideas`;
23+
const response = await authenticatedFetch(url, {
24+
method: "POST",
25+
body: JSON.stringify({ target_creator_id: targetCreatorId }),
26+
});
27+
28+
if (!response.ok) {
29+
const errorData = await response.json().catch(() => ({}));
30+
throw new Error(
31+
errorData.detail || `Failed to generate collaboration ideas: ${response.statusText}`
32+
);
33+
}
34+
35+
return response.json();
36+
}
37+
38+
export interface CreatorRecommendationForIdea {
39+
creator_id: string;
40+
display_name: string;
41+
profile_picture_url: string | null;
42+
primary_niche: string;
43+
match_score: number;
44+
reasoning: string;
45+
}
46+
47+
export interface RecommendCreatorResponse {
48+
recommended_creator: CreatorRecommendationForIdea;
49+
alternatives: CreatorRecommendationForIdea[];
50+
}
51+
52+
/**
53+
* Recommend the best creator from a list of candidates for a collaboration idea
54+
*/
55+
export async function recommendCreatorForIdea(
56+
collaborationIdea: string,
57+
candidateCreatorIds: string[]
58+
): Promise<RecommendCreatorResponse> {
59+
const url = `${API_BASE_URL}/collaborations/recommend-creator`;
60+
const response = await authenticatedFetch(url, {
61+
method: "POST",
62+
body: JSON.stringify({
63+
collaboration_idea: collaborationIdea,
64+
candidate_creator_ids: candidateCreatorIds,
65+
}),
66+
});
67+
68+
if (!response.ok) {
69+
const errorData = await response.json().catch(() => ({}));
70+
throw new Error(
71+
errorData.detail || `Failed to get recommendation: ${response.statusText}`
72+
);
73+
}
74+
75+
return response.json();
76+
}
77+

frontend/lib/api/creators.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { authenticatedFetch } from "../auth-helpers";
2+
3+
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
4+
5+
export interface CreatorBasic {
6+
id: string;
7+
display_name: string;
8+
tagline: string | null;
9+
bio: string | null;
10+
profile_picture_url: string | null;
11+
primary_niche: string;
12+
secondary_niches: string[] | null;
13+
total_followers: number;
14+
engagement_rate: number | null;
15+
is_verified_creator: boolean;
16+
profile_completion_percentage: number;
17+
}
18+
19+
export interface CreatorFull extends CreatorBasic {
20+
user_id: string;
21+
cover_image_url: string | null;
22+
website_url: string | null;
23+
youtube_url: string | null;
24+
youtube_handle: string | null;
25+
youtube_subscribers: number | null;
26+
instagram_url: string | null;
27+
instagram_handle: string | null;
28+
instagram_followers: number | null;
29+
tiktok_url: string | null;
30+
tiktok_handle: string | null;
31+
tiktok_followers: number | null;
32+
twitter_url: string | null;
33+
twitter_handle: string | null;
34+
twitter_followers: number | null;
35+
twitch_url: string | null;
36+
twitch_handle: string | null;
37+
twitch_followers: number | null;
38+
linkedin_url: string | null;
39+
facebook_url: string | null;
40+
content_types: string[] | null;
41+
content_language: string[] | null;
42+
total_reach: number | null;
43+
average_views: number | null;
44+
audience_age_primary: string | null;
45+
audience_gender_split: Record<string, any> | null;
46+
audience_locations: Record<string, any> | null;
47+
audience_interests: string[] | null;
48+
average_engagement_per_post: number | null;
49+
posting_frequency: string | null;
50+
best_performing_content_type: string | null;
51+
years_of_experience: number | null;
52+
content_creation_full_time: boolean;
53+
team_size: number;
54+
equipment_quality: string | null;
55+
editing_software: string[] | null;
56+
collaboration_types: string[] | null;
57+
preferred_brands_style: string[] | null;
58+
rate_per_post: number | null;
59+
rate_per_video: number | null;
60+
rate_per_story: number | null;
61+
rate_per_reel: number | null;
62+
rate_negotiable: boolean;
63+
accepts_product_only_deals: boolean;
64+
minimum_deal_value: number | null;
65+
preferred_payment_terms: string | null;
66+
portfolio_links: string[] | null;
67+
past_brand_collaborations: string[] | null;
68+
case_study_links: string[] | null;
69+
media_kit_url: string | null;
70+
created_at: string | null;
71+
last_active_at: string | null;
72+
}
73+
74+
export interface ListCreatorsParams {
75+
search?: string;
76+
niche?: string;
77+
limit?: number;
78+
offset?: number;
79+
}
80+
81+
/**
82+
* List all creators with optional search and filters
83+
*/
84+
export async function listCreators(
85+
params: ListCreatorsParams = {}
86+
): Promise<CreatorBasic[]> {
87+
const queryParams = new URLSearchParams();
88+
if (params.search) queryParams.append("search", params.search);
89+
if (params.niche) queryParams.append("niche", params.niche);
90+
if (params.limit) queryParams.append("limit", params.limit.toString());
91+
if (params.offset) queryParams.append("offset", params.offset.toString());
92+
93+
const url = `${API_BASE_URL}/creators?${queryParams.toString()}`;
94+
const response = await authenticatedFetch(url);
95+
96+
if (!response.ok) {
97+
// Try to get error details from response
98+
let errorMessage = `Failed to fetch creators: ${response.statusText}`;
99+
try {
100+
const errorData = await response.json();
101+
if (errorData.detail) {
102+
errorMessage = errorData.detail;
103+
}
104+
} catch {
105+
// If response is not JSON, use status text
106+
}
107+
108+
const error = new Error(errorMessage);
109+
(error as any).status = response.status;
110+
throw error;
111+
}
112+
113+
return response.json();
114+
}
115+
116+
/**
117+
* Get full details of a specific creator
118+
*/
119+
export async function getCreatorDetails(creatorId: string): Promise<CreatorFull> {
120+
const url = `${API_BASE_URL}/creators/${creatorId}`;
121+
const response = await authenticatedFetch(url);
122+
123+
if (!response.ok) {
124+
throw new Error(`Failed to fetch creator details: ${response.statusText}`);
125+
}
126+
127+
return response.json();
128+
}
129+
130+
export interface CreatorRecommendation {
131+
id: string;
132+
display_name: string;
133+
profile_picture_url: string | null;
134+
primary_niche: string | null;
135+
total_followers: number | null;
136+
engagement_rate: number | null;
137+
top_platforms?: string[] | null;
138+
match_score: number;
139+
reason: string;
140+
}
141+
142+
export async function getCreatorRecommendations(limit = 4): Promise<CreatorRecommendation[]> {
143+
const url = `${API_BASE_URL}/creators/recommendations?limit=${limit}`;
144+
const response = await authenticatedFetch(url);
145+
146+
if (!response.ok) {
147+
const errText = await response.text().catch(() => "");
148+
throw new Error(`Failed to fetch recommendations: ${response.status} ${errText}`);
149+
}
150+
return response.json();
151+
}
152+
153+
/**
154+
* Get list of all unique niches
155+
*/
156+
export async function listNiches(): Promise<string[]> {
157+
const url = `${API_BASE_URL}/creators/niches/list`;
158+
const response = await authenticatedFetch(url);
159+
160+
if (!response.ok) {
161+
throw new Error(`Failed to fetch niches: ${response.statusText}`);
162+
}
163+
164+
const data = await response.json();
165+
return data.niches || [];
166+
}
167+

frontend/lib/auth-helpers.ts

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { supabase } from "./supabaseClient";
1+
import { getAuthToken, supabase } from "./supabaseClient";
22

33
export interface UserProfile {
44
id: string;
@@ -16,7 +16,9 @@ export async function getCurrentUser() {
1616
data: { user },
1717
error,
1818
} = await supabase.auth.getUser();
19-
if (error) throw error;
19+
if (error || !user) {
20+
return null;
21+
}
2022
return user;
2123
}
2224

@@ -34,7 +36,10 @@ export async function getUserProfile(): Promise<UserProfile | null> {
3436
.eq("id", user.id)
3537
.single();
3638

37-
if (error) throw error;
39+
if (error) {
40+
console.error("Error fetching profile:", error);
41+
return null;
42+
}
3843
return data as UserProfile;
3944
} catch (error) {
4045
console.error("Error fetching user profile:", error);
@@ -53,16 +58,9 @@ export async function signOut() {
5358
/**
5459
* Check if user has a specific role
5560
*/
56-
export async function checkUserRole(
57-
requiredRole: "Creator" | "Brand"
58-
): Promise<boolean> {
59-
try {
60-
const profile = await getUserProfile();
61-
return profile?.role === requiredRole;
62-
} catch (error) {
63-
console.error("Error checking user role:", error);
64-
return false;
65-
}
61+
export async function checkUserRole(): Promise<"Creator" | "Brand" | null> {
62+
const profile = await getUserProfile();
63+
return profile?.role || null;
6664
}
6765

6866
/**
@@ -78,6 +76,88 @@ export async function hasCompletedOnboarding(): Promise<boolean> {
7876
}
7977
}
8078

79+
/**
80+
* Token refresh handling (automatic with Supabase)
81+
*/
82+
export async function ensureValidToken() {
83+
const { data: { session } } = await supabase.auth.getSession();
84+
85+
if (!session) {
86+
throw new Error("No active session");
87+
}
88+
89+
// Supabase automatically refreshes if needed
90+
return session.access_token;
91+
}
92+
93+
/**
94+
* Make authenticated API calls to backend
95+
*/
96+
export async function authenticatedFetch(url: string, options: RequestInit = {}) {
97+
// Try to get token, refresh if needed
98+
let token = await getAuthToken();
99+
100+
// If no token, try to refresh session
101+
if (!token) {
102+
try {
103+
const { data: { session }, error } = await supabase.auth.refreshSession();
104+
if (error) {
105+
console.error("Failed to refresh session:", error);
106+
// Don't throw here - let the 401 response handle it
107+
token = undefined;
108+
} else {
109+
token = session?.access_token || undefined;
110+
}
111+
} catch (err) {
112+
console.error("Error refreshing token:", err);
113+
token = undefined;
114+
}
115+
}
116+
117+
if (!token) {
118+
// Return a response that will trigger 401, don't throw
119+
// This allows the calling code to handle it gracefully
120+
return new Response(
121+
JSON.stringify({ error: "No authentication token available" }),
122+
{ status: 401, headers: { "Content-Type": "application/json" } }
123+
);
124+
}
125+
126+
const headers = {
127+
...options.headers,
128+
"Authorization": `Bearer ${token}`,
129+
"Content-Type": "application/json",
130+
};
131+
132+
const response = await fetch(url, {
133+
...options,
134+
headers,
135+
});
136+
137+
// If 401, try refreshing token once
138+
if (response.status === 401) {
139+
try {
140+
const { data: { session }, error } = await supabase.auth.refreshSession();
141+
if (!error && session?.access_token) {
142+
// Retry with new token
143+
const retryHeaders = {
144+
...options.headers,
145+
"Authorization": `Bearer ${session.access_token}`,
146+
"Content-Type": "application/json",
147+
};
148+
return fetch(url, {
149+
...options,
150+
headers: retryHeaders,
151+
});
152+
}
153+
} catch (err) {
154+
console.error("Error refreshing token on 401:", err);
155+
}
156+
}
157+
158+
return response;
159+
}
160+
81161
/**
82162
* Map Supabase auth errors to user-friendly messages
83163
*/

0 commit comments

Comments
 (0)