mono/packages/ui/src/modules/user/client-user.ts
2026-02-18 22:44:09 +01:00

509 lines
18 KiB
TypeScript

import { supabase as defaultSupabase } from "@/integrations/supabase/client";
import { UserProfile } from "@/pages/Post/types";
import { SupabaseClient } from "@supabase/supabase-js";
import { fetchWithDeduplication } from "@/lib/db";
/** Fetch full profile data from server API endpoint */
export const fetchProfileAPI = async (userId: string): Promise<{ profile: any; recentPosts: any[] } | null> => {
const res = await fetch(`/api/profile/${userId}`);
if (!res.ok) {
if (res.status === 404) return null;
throw new Error(`Failed to fetch profile: ${res.statusText}`);
}
return await res.json();
};
export const fetchAuthorProfile = async (userId: string, client?: SupabaseClient): Promise<UserProfile | null> => {
const supabase = client || defaultSupabase;
return fetchWithDeduplication(`profile-${userId}`, async () => {
console.log('Fetching profile for user:', userId);
const { data, error } = await supabase
.from('profiles')
.select('user_id, avatar_url, display_name, username')
.eq('user_id', userId)
.single();
if (error && error.code !== 'PGRST116') throw error;
return data as UserProfile;
});
};
export const getUserSettings = async (userId: string, client?: SupabaseClient) => {
return fetchWithDeduplication(`settings-${userId}`, async () => {
const token = await getAuthToken();
const res = await fetch('/api/me/settings', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error(`Failed to fetch settings: ${res.statusText}`);
return await res.json();
}, 100000);
};
export const updateUserSettings = async (userId: string, settings: any, client?: SupabaseClient) => {
const token = await getAuthToken();
const res = await fetch('/api/me/settings', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(settings)
});
if (!res.ok) throw new Error(`Failed to update settings: ${res.statusText}`);
};
export const getUserOpenAIKey = async (userId: string, client?: SupabaseClient) => {
const supabase = client || defaultSupabase;
return fetchWithDeduplication(`openai-${userId}`, async () => {
const { data, error } = await supabase
.from('user_secrets')
.select('settings')
.eq('user_id', userId)
.maybeSingle();
if (error) throw error;
const settings = data?.settings as any;
return settings?.api_keys?.openai_api_key;
});
}
/** Get all API keys from user_secrets.settings.api_keys */
export const getUserApiKeys = async (userId: string, client?: SupabaseClient): Promise<Record<string, string> | null> => {
const supabase = client || defaultSupabase;
return fetchWithDeduplication(`api-keys-${userId}`, async () => {
const { data, error } = await supabase
.from('user_secrets')
.select('settings')
.eq('user_id', userId)
.maybeSingle();
if (error) throw error;
const settings = data?.settings as any;
return (settings?.api_keys as Record<string, string>) || null;
});
};
export const getUserGoogleApiKey = async (userId: string, client?: SupabaseClient) => {
const supabase = client || defaultSupabase;
return fetchWithDeduplication(`google-${userId}`, async () => {
const { data, error } = await supabase
.from('user_secrets')
.select('settings')
.eq('user_id', userId)
.maybeSingle();
if (error) throw error;
const settings = data?.settings as any;
return settings?.api_keys?.google_api_key;
});
}
export const getUserSecrets = async (userId: string, client?: SupabaseClient) => {
const supabase = client || defaultSupabase;
return fetchWithDeduplication(`user-secrets-${userId}`, async () => {
console.log('Fetching user secrets for user:', userId);
const { data, error } = await supabase
.from('user_secrets')
.select('settings')
.eq('user_id', userId)
.maybeSingle();
if (error) throw error;
// Return whole settings object or specific part?
// Instructions involve variables in settings.variables
const settings = data?.settings as any;
return settings?.variables || {};
});
};
export const getProviderConfig = async (userId: string, provider: string, client?: SupabaseClient) => {
return fetchWithDeduplication(`provider-${userId}-${provider}`, async () => {
const token = await getAuthToken();
const res = await fetch(`/api/me/provider-config/${encodeURIComponent(provider)}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.status === 404) return null;
if (!res.ok) throw new Error(`Failed to fetch provider config: ${res.statusText}`);
return await res.json();
});
}
export const fetchUserRoles = async (userId: string, client?: SupabaseClient) => {
return fetchWithDeduplication(`roles-${userId}`, async () => {
const token = await getAuthToken();
const res = await fetch('/api/me/roles', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) {
console.error('Error fetching user roles:', res.statusText);
return [];
}
return await res.json();
});
};
/**
* Update user secrets in user_secrets table (settings column)
*/
export const updateUserSecrets = async (userId: string, secrets: Record<string, string>): Promise<void> => {
try {
// Check if record exists
const { data: existing } = await defaultSupabase
.from('user_secrets')
.select('settings')
.eq('user_id', userId)
.maybeSingle();
if (existing) {
// Update existing
const currentSettings = (existing.settings as Record<string, any>) || {};
const currentApiKeys = (currentSettings.api_keys as Record<string, any>) || {};
const newSettings = {
...currentSettings,
api_keys: { ...currentApiKeys, ...secrets }
};
const { error } = await defaultSupabase
.from('user_secrets')
.update({ settings: newSettings })
.eq('user_id', userId);
if (error) throw error;
} else {
// Insert new
const { error } = await defaultSupabase
.from('user_secrets')
.insert({
user_id: userId,
settings: { api_keys: secrets }
});
if (error) throw error;
}
} catch (error) {
console.error('Error updating user secrets:', error);
throw error;
}
};
/**
* Get user variables from user_secrets table (settings.variables)
*/
export const getUserVariables = async (userId: string): Promise<Record<string, any> | null> => {
console.log('getUserVariables Fetching user variables for user:', userId);
try {
const { data: secretData } = await defaultSupabase
.from('user_secrets')
.select('settings')
.eq('user_id', userId)
.single();
if (!secretData?.settings) return null;
const settings = secretData.settings as Record<string, any>;
return (settings.variables as Record<string, any>) || {};
} catch (error) {
console.error('Error fetching user variables:', error);
return null;
}
};
/**
* Update user variables in user_secrets table (settings.variables)
*/
export const updateUserVariables = async (userId: string, variables: Record<string, any>): Promise<void> => {
console.log('Updating user variables for user:', userId);
try {
// Check if record exists
const { data: existing } = await defaultSupabase
.from('user_secrets')
.select('settings')
.eq('user_id', userId)
.maybeSingle();
if (existing) {
// Update existing
const currentSettings = (existing.settings as Record<string, any>) || {};
const newSettings = {
...currentSettings,
variables: variables // Replace variables entirely with new state (since editor manages full set)
};
const { error } = await defaultSupabase
.from('user_secrets')
.update({ settings: newSettings })
.eq('user_id', userId);
if (error) throw error;
} else {
// Insert new
const { error } = await defaultSupabase
.from('user_secrets')
.insert({
user_id: userId,
settings: { variables: variables }
});
if (error) throw error;
}
} catch (error) {
console.error('Error updating user variables:', error);
throw error;
}
};
// =============================================
// Shipping Addresses (stored in user_secrets.settings.shipping_addresses)
// =============================================
export interface SavedShippingAddress {
id: string;
label: string;
fullName: string;
email: string;
phone: string;
address: string;
city: string;
zip: string;
country: string;
note: string;
isDefault: boolean;
}
/** Get shipping addresses from user_secrets.settings.shipping_addresses */
export const getShippingAddresses = async (userId: string, client?: SupabaseClient): Promise<SavedShippingAddress[]> => {
const supabase = client || defaultSupabase;
try {
const { data, error } = await supabase
.from('user_secrets')
.select('settings')
.eq('user_id', userId)
.maybeSingle();
if (error) throw error;
const settings = data?.settings as any;
return (settings?.shipping_addresses as SavedShippingAddress[]) || [];
} catch (error) {
console.error('Error fetching shipping addresses:', error);
return [];
}
};
/** Save shipping addresses to user_secrets.settings.shipping_addresses (full replace) */
export const saveShippingAddresses = async (userId: string, addresses: SavedShippingAddress[], client?: SupabaseClient): Promise<void> => {
const supabase = client || defaultSupabase;
try {
const { data: existing } = await supabase
.from('user_secrets')
.select('settings')
.eq('user_id', userId)
.maybeSingle();
if (existing) {
const currentSettings = (existing.settings as Record<string, any>) || {};
const newSettings = { ...currentSettings, shipping_addresses: addresses };
const { error } = await supabase
.from('user_secrets')
.update({ settings: newSettings })
.eq('user_id', userId);
if (error) throw error;
} else {
const { error } = await supabase
.from('user_secrets')
.insert({
user_id: userId,
settings: { shipping_addresses: addresses }
});
if (error) throw error;
}
} catch (error) {
console.error('Error saving shipping addresses:', error);
throw error;
}
};
// =============================================
// Vendor Profiles (stored in user_secrets.settings.vendor_profiles)
// =============================================
export interface VendorProfile {
id: string;
label: string;
companyName: string;
email: string;
phone: string;
vatId: string;
eoriNumber: string;
logoUrl: string;
address: string;
city: string;
zip: string;
country: string;
defaultCurrency: string;
defaultCountryOfOrigin: string;
note: string;
isDefault: boolean;
}
/** Get vendor profiles from user_secrets.settings.vendor_profiles */
export const getVendorProfiles = async (userId: string, client?: SupabaseClient): Promise<VendorProfile[]> => {
const supabase = client || defaultSupabase;
try {
const { data, error } = await supabase
.from('user_secrets')
.select('settings')
.eq('user_id', userId)
.maybeSingle();
if (error) throw error;
const settings = data?.settings as any;
return (settings?.vendor_profiles as VendorProfile[]) || [];
} catch (error) {
console.error('Error fetching vendor profiles:', error);
return [];
}
};
/** Save vendor profiles to user_secrets.settings.vendor_profiles (full replace) */
export const saveVendorProfiles = async (userId: string, profiles: VendorProfile[], client?: SupabaseClient): Promise<void> => {
const supabase = client || defaultSupabase;
try {
const { data: existing } = await supabase
.from('user_secrets')
.select('settings')
.eq('user_id', userId)
.maybeSingle();
if (existing) {
const currentSettings = (existing.settings as Record<string, any>) || {};
const newSettings = { ...currentSettings, vendor_profiles: profiles };
const { error } = await supabase
.from('user_secrets')
.update({ settings: newSettings })
.eq('user_id', userId);
if (error) throw error;
} else {
const { error } = await supabase
.from('user_secrets')
.insert({
user_id: userId,
settings: { vendor_profiles: profiles }
});
if (error) throw error;
}
} catch (error) {
console.error('Error saving vendor profiles:', error);
throw error;
}
};
/** Update the current user's profile via API */
export const updateProfileAPI = async (profileData: {
username?: string | null;
display_name?: string | null;
bio?: string | null;
avatar_url?: string | null;
settings?: any;
}): Promise<any> => {
const { data: sessionData } = await defaultSupabase.auth.getSession();
const token = sessionData.session?.access_token;
if (!token) throw new Error('Not authenticated');
const res = await fetch('/api/profile', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(profileData)
});
if (!res.ok) throw new Error(`Failed to update profile: ${res.statusText}`);
return await res.json();
};
/** Update the current user's email (auth-level operation) */
export const updateUserEmail = async (newEmail: string): Promise<void> => {
const { error } = await defaultSupabase.auth.updateUser({ email: newEmail });
if (error) throw error;
};
// =============================================
// Admin User Management API Wrappers
// =============================================
const getAuthToken = async (): Promise<string> => {
const { data: sessionData } = await defaultSupabase.auth.getSession();
const token = sessionData.session?.access_token;
if (!token) throw new Error('Not authenticated');
return token;
};
/** Fetch all users (admin) */
export const fetchAdminUsersAPI = async (): Promise<any[]> => {
const token = await getAuthToken();
const res = await fetch('/api/admin/users', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error(`Failed to fetch users: ${res.statusText}`);
return await res.json();
};
/** Create a new user (admin) */
export const createAdminUserAPI = async (email: string, password: string, copySettings: boolean): Promise<any> => {
const token = await getAuthToken();
const res = await fetch('/api/admin/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ email, password, copySettings })
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || `Failed to create user: ${res.statusText}`);
}
return await res.json();
};
/** Update a user's profile (admin) */
export const updateAdminUserAPI = async (userId: string, profile: {
username?: string | null;
display_name?: string | null;
bio?: string | null;
}): Promise<any> => {
const token = await getAuthToken();
const res = await fetch(`/api/admin/users/${userId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(profile)
});
if (!res.ok) throw new Error(`Failed to update user: ${res.statusText}`);
return await res.json();
};
/** Delete a user (admin) */
export const deleteAdminUserAPI = async (userId: string): Promise<void> => {
const token = await getAuthToken();
const res = await fetch(`/api/admin/users/${userId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error(`Failed to delete user: ${res.statusText}`);
};