diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 1cbe6b83..4e1f9073 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -25,7 +25,6 @@ const GlobalDragDrop = React.lazy(() => import("@/components/GlobalDragDrop")); registerAllWidgets(); import Index from "./pages/Index"; -import Auth from "./pages/Auth"; import AuthZ from "./pages/AuthZ"; const UpdatePassword = React.lazy(() => import("./pages/UpdatePassword")); @@ -74,8 +73,6 @@ TypesPlayground = React.lazy(() => import("@/modules/types/TypesPlayground")); if (enablePlaygrounds) { PlaygroundEditor = React.lazy(() => import("./pages/PlaygroundEditor")); PlaygroundEditorLLM = React.lazy(() => import("./pages/PlaygroundEditorLLM")); - VideoPlayerPlayground = React.lazy(() => import("./pages/VideoPlayerPlayground")); - VideoFeedPlayground = React.lazy(() => import("./pages/VideoFeedPlayground")); VideoPlayerPlaygroundIntern = React.lazy(() => import("./pages/VideoPlayerPlaygroundIntern")); PlaygroundImages = React.lazy(() => import("./pages/PlaygroundImages")); PlaygroundImageEditor = React.lazy(() => import("./pages/PlaygroundImageEditor")); @@ -149,7 +146,7 @@ const AppWrapper = () => { {/* Top-level routes (no organization context) */} } /> - } /> + } /> } /> Loading...}>} /> } /> @@ -279,7 +276,10 @@ const App = () => { new Map() }}> window.history.replaceState({}, document.title, window.location.pathname)} + onSigninCallback={(user) => { + const redirectTo = (user?.state as any)?.redirectTo ?? '/'; + window.location.replace(redirectTo); + }} > diff --git a/packages/ui/src/components/AddToCollectionModal.tsx b/packages/ui/src/components/AddToCollectionModal.tsx index 410dd339..e4ffa891 100644 --- a/packages/ui/src/components/AddToCollectionModal.tsx +++ b/packages/ui/src/components/AddToCollectionModal.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { supabase } from '@/integrations/supabase/client'; +import { apiClient } from '@/lib/db'; import { useAuth } from '@/hooks/useAuth'; import { Dialog, @@ -57,50 +57,26 @@ const AddToCollectionModal = ({ isOpen, onClose, pictureId, postId }: AddToColle const fetchCollections = async () => { if (!user) return; - - const { data, error } = await supabase - .from('collections') - .select('*') - .eq('user_id', user.id) - .order('created_at', { ascending: false }); - - if (error) { + try { + const data = await apiClient(`/api/collections?userId=${user.id}`); + setCollections(data || []); + } catch (error) { console.error('Error fetching collections:', error); - return; } - - setCollections(data || []); }; const fetchItemCollections = async () => { if (!user) return; - - if (postId) { - // Fetch for post - const { data, error } = await supabase - .from('collection_posts' as any) // Cast as any until types are generated - .select('collection_id') - .eq('post_id', postId); - - if (error) { - console.error('Error fetching post collections:', error); - return; + try { + if (postId) { + const data = await apiClient<{ collection_id: string }[]>(`/api/collection-pictures?postId=${postId}`); + setSelectedCollections(new Set(data.map((item: any) => item.collection_id))); + } else if (pictureId) { + const data = await apiClient<{ collection_id: string }[]>(`/api/collection-pictures?pictureId=${pictureId}`); + setSelectedCollections(new Set(data.map(item => item.collection_id))); } - const collectionIds = new Set(data.map((item: any) => item.collection_id)); - setSelectedCollections(collectionIds); - } else if (pictureId) { - // Fetch for picture - const { data, error } = await supabase - .from('collection_pictures') - .select('collection_id') - .eq('picture_id', pictureId); - - if (error) { - console.error('Error fetching picture collections:', error); - return; - } - const collectionIds = new Set(data.map(item => item.collection_id)); - setSelectedCollections(collectionIds); + } catch (error) { + console.error('Error fetching item collections:', error); } }; @@ -119,56 +95,35 @@ const AddToCollectionModal = ({ isOpen, onClose, pictureId, postId }: AddToColle setLoading(true); const slug = createSlug(newCollection.name); - const { data, error } = await supabase - .from('collections') - .insert({ - user_id: user.id, - name: newCollection.name.trim(), - description: newCollection.description.trim() || null, - slug, - is_public: newCollection.is_public - }) - .select() - .single(); - - if (error) { - console.error('Error creating collection:', error); - toast({ - title: "Error", - description: "Failed to create collection", - variant: "destructive" + try { + const data = await apiClient('/api/collections', { + method: 'POST', + body: JSON.stringify({ + user_id: user.id, + name: newCollection.name.trim(), + description: newCollection.description.trim() || null, + slug, + is_public: newCollection.is_public, + }), }); + + if (postId) { + await apiClient('/api/collection-pictures', { method: 'POST', body: JSON.stringify({ inserts: [{ collection_id: data.id, post_id: postId }] }) }); + } else if (pictureId) { + await apiClient('/api/collection-pictures', { method: 'POST', body: JSON.stringify({ inserts: [{ collection_id: data.id, picture_id: pictureId }] }) }); + } + + setCollections(prev => [data, ...prev]); + setSelectedCollections(prev => new Set([...prev, data.id])); + setNewCollection({ name: '', description: '', is_public: true }); + setShowCreateForm(false); + toast({ title: "Success", description: "Collection created and photo added!" }); + } catch (error) { + console.error('Error creating collection:', error); + toast({ title: "Error", description: "Failed to create collection", variant: "destructive" }); + } finally { setLoading(false); - return; } - - // Add item to new collection - if (postId) { - await supabase - .from('collection_posts' as any) - .insert({ - collection_id: data.id, - post_id: postId - }); - } else if (pictureId) { - await supabase - .from('collection_pictures') - .insert({ - collection_id: data.id, - picture_id: pictureId - }); - } - - setCollections(prev => [data, ...prev]); - setSelectedCollections(prev => new Set([...prev, data.id])); - setNewCollection({ name: '', description: '', is_public: true }); - setShowCreateForm(false); - setLoading(false); - - toast({ - title: "Success", - description: "Collection created and photo added!" - }); }; const handleToggleCollection = async (collectionId: string) => { @@ -176,50 +131,26 @@ const AddToCollectionModal = ({ isOpen, onClose, pictureId, postId }: AddToColle const isSelected = selectedCollections.has(collectionId); - if (isSelected) { - // Remove from collection - if (postId) { - const { error } = await supabase - .from('collection_posts' as any) - .delete() - .eq('collection_id', collectionId) - .eq('post_id', postId); - if (error) console.error('Error removing post from collection:', error); - } else if (pictureId) { - const { error } = await supabase - .from('collection_pictures') - .delete() - .eq('collection_id', collectionId) - .eq('picture_id', pictureId); - if (error) console.error('Error removing picture from collection:', error); + try { + if (isSelected) { + const body = postId + ? { collection_id: collectionId, post_id: postId } + : { collection_id: collectionId, picture_id: pictureId }; + await apiClient('/api/collection-pictures', { method: 'DELETE', body: JSON.stringify(body) }); + setSelectedCollections(prev => { + const newSet = new Set(prev); + newSet.delete(collectionId); + return newSet; + }); + } else { + const insert = postId + ? { collection_id: collectionId, post_id: postId } + : { collection_id: collectionId, picture_id: pictureId }; + await apiClient('/api/collection-pictures', { method: 'POST', body: JSON.stringify({ inserts: [insert] }) }); + setSelectedCollections(prev => new Set([...prev, collectionId])); } - - setSelectedCollections(prev => { - const newSet = new Set(prev); - newSet.delete(collectionId); - return newSet; - }); - } else { - // Add to collection - if (postId) { - const { error } = await supabase - .from('collection_posts' as any) - .insert({ - collection_id: collectionId, - post_id: postId - }); - if (error) console.error('Error adding post to collection:', error); - } else if (pictureId) { - const { error } = await supabase - .from('collection_pictures') - .insert({ - collection_id: collectionId, - picture_id: pictureId - }); - if (error) console.error('Error adding picture to collection:', error); - } - - setSelectedCollections(prev => new Set([...prev, collectionId])); + } catch (error) { + console.error('Error toggling collection:', error); } }; diff --git a/packages/ui/src/components/CreationWizardPopup.tsx b/packages/ui/src/components/CreationWizardPopup.tsx index 30cc53e0..2d43bd4f 100644 --- a/packages/ui/src/components/CreationWizardPopup.tsx +++ b/packages/ui/src/components/CreationWizardPopup.tsx @@ -19,7 +19,7 @@ import { useMediaRefresh } from '@/contexts/MediaRefreshContext'; import { createPicture } from '@/modules/posts/client-pictures'; import { fetchPostById } from '@/modules/posts/client-posts'; import { toast } from 'sonner'; -import { supabase } from '@/integrations/supabase/client'; +import { getAuthToken } from '@/lib/db'; import { serverUrl } from '@/lib/db'; interface CreationWizardPopupProps { @@ -293,8 +293,8 @@ export const CreationWizardPopup: React.FC = ({ return; } - const { data: { session: authSession } } = await supabase.auth.getSession(); - if (!authSession?.access_token) { + const token = await getAuthToken(); + if (!token) { toast.error(translate('Please sign in to upload files')); return; } @@ -309,7 +309,7 @@ export const CreationWizardPopup: React.FC = ({ const file = img.file as File; const xhr = new XMLHttpRequest(); xhr.open('POST', `${serverUrl}/api/vfs/upload/${mount}/${encodeURIComponent(file.name)}`, true); - xhr.setRequestHeader('Authorization', `Bearer ${authSession.access_token}`); + xhr.setRequestHeader('Authorization', `Bearer ${token}`); xhr.upload.onprogress = (evt) => { if (evt.lengthComputable) { @@ -348,8 +348,7 @@ export const CreationWizardPopup: React.FC = ({ // Helper function to upload internal video const uploadInternalVideo = async (file: File): Promise => { - // Get auth token before creating the XHR - const { data: { session: authSession } } = await supabase.auth.getSession(); + const videoToken = await getAuthToken(); return new Promise((resolve, reject) => { const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL; @@ -360,8 +359,8 @@ export const CreationWizardPopup: React.FC = ({ const xhr = new XMLHttpRequest(); xhr.open('POST', `${serverUrl}/api/videos/upload?userId=${user?.id}&title=${encodeURIComponent(title)}&preset=original`); - if (authSession?.access_token) { - xhr.setRequestHeader('Authorization', `Bearer ${authSession.access_token}`); + if (videoToken) { + xhr.setRequestHeader('Authorization', `Bearer ${videoToken}`); } xhr.upload.onprogress = (e) => { diff --git a/packages/ui/src/components/EditImageModal.tsx b/packages/ui/src/components/EditImageModal.tsx index 36f06112..032afc4f 100644 --- a/packages/ui/src/components/EditImageModal.tsx +++ b/packages/ui/src/components/EditImageModal.tsx @@ -8,7 +8,7 @@ import { Switch } from '@/components/ui/switch'; import { Checkbox } from '@/components/ui/checkbox'; import { Card, CardContent } from '@/components/ui/card'; import { useForm } from 'react-hook-form'; -import { supabase } from '@/integrations/supabase/client'; +import { apiClient } from '@/lib/db'; import { updatePicture } from '@/modules/posts/client-pictures'; import { useAuth } from '@/hooks/useAuth'; import { toast } from 'sonner'; @@ -90,13 +90,7 @@ const EditImageModal = ({ setLoadingCollections(true); try { - const { data, error } = await supabase - .from('collections') - .select('*') - .eq('user_id', user.id) - .order('created_at', { ascending: false }); - - if (error) throw error; + const data = await apiClient(`/api/collections?userId=${user.id}`); setCollections(data || []); } catch (error) { console.error('Error fetching collections:', error); @@ -110,15 +104,8 @@ const EditImageModal = ({ if (!user) return; try { - const { data, error } = await supabase - .from('collection_pictures') - .select('collection_id') - .eq('picture_id', pictureId); - - if (error) throw error; - - const collectionIds = new Set(data.map(item => item.collection_id)); - setSelectedCollections(collectionIds); + const data = await apiClient<{ collection_id: string }[]>(`/api/collection-pictures?pictureId=${pictureId}`); + setSelectedCollections(new Set(data.map(item => item.collection_id))); } catch (error) { console.error('Error fetching picture collections:', error); } @@ -131,33 +118,21 @@ const EditImageModal = ({ try { if (isSelected) { - // Remove from collection - const { error } = await supabase - .from('collection_pictures') - .delete() - .eq('collection_id', collectionId) - .eq('picture_id', pictureId); - - if (error) throw error; - + await apiClient(`/api/collection-pictures`, { + method: 'DELETE', + body: JSON.stringify({ collection_id: collectionId, picture_id: pictureId }), + }); setSelectedCollections(prev => { const newSet = new Set(prev); newSet.delete(collectionId); return newSet; }); - toast.success(translate('Removed from collection')); } else { - // Add to collection - const { error } = await supabase - .from('collection_pictures') - .insert({ - collection_id: collectionId, - picture_id: pictureId - }); - - if (error) throw error; - + await apiClient(`/api/collection-pictures`, { + method: 'POST', + body: JSON.stringify({ inserts: [{ collection_id: collectionId, picture_id: pictureId }] }), + }); setSelectedCollections(prev => new Set([...prev, collectionId])); toast.success(translate('Added to collection')); } diff --git a/packages/ui/src/components/EditVideoModal.tsx b/packages/ui/src/components/EditVideoModal.tsx index 7d5976d4..e2ed158b 100644 --- a/packages/ui/src/components/EditVideoModal.tsx +++ b/packages/ui/src/components/EditVideoModal.tsx @@ -10,7 +10,8 @@ import { Card, CardContent } from '@/components/ui/card'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; -import { supabase } from '@/integrations/supabase/client'; +import { apiClient } from '@/lib/db'; +import { updatePicture } from '@/modules/posts/client-pictures'; import { useAuth } from '@/hooks/useAuth'; import { toast } from 'sonner'; import { Mic, MicOff, Loader2, Bookmark } from 'lucide-react'; @@ -89,13 +90,7 @@ const EditVideoModal = ({ setLoadingCollections(true); try { - const { data, error } = await supabase - .from('collections') - .select('*') - .eq('user_id', user.id) - .order('created_at', { ascending: false }); - - if (error) throw error; + const data = await apiClient(`/api/collections?userId=${user.id}`); setCollections(data || []); } catch (error) { console.error('Error fetching collections:', error); @@ -109,18 +104,8 @@ const EditVideoModal = ({ if (!user) return; try { - // Reuse collection_pictures table with picture_id field for videos - const { data, error } = await supabase - .from('collection_pictures') - .select('collection_id') - .eq('picture_id', videoId); - - if (error) throw error; - - if (data) { - const collectionIds = data.map(cp => cp.collection_id); - setSelectedCollections(new Set(collectionIds)); - } + const data = await apiClient<{ collection_id: string }[]>(`/api/collection-pictures?pictureId=${videoId}`); + setSelectedCollections(new Set(data.map(cp => cp.collection_id))); } catch (error) { console.error('Error fetching video collections:', error); } @@ -141,44 +126,28 @@ const EditVideoModal = ({ setUpdating(true); try { - // Update video metadata in pictures table - const { error: updateError } = await supabase - .from('pictures') - .update({ - title: data.title?.trim() || null, - description: data.description?.trim() || null, - visible: data.visible, - updated_at: new Date().toISOString(), - }) - .eq('id', videoId) - .eq('user_id', user.id) - .eq('type', 'mux-video'); + await updatePicture(videoId, { + title: data.title?.trim() || null, + description: data.description?.trim() || null, + visible: data.visible, + updated_at: new Date().toISOString(), + } as any); - if (updateError) throw updateError; - - // Update collections (reuse collection_pictures table with picture_id for videos) try { - // First, remove all existing collection associations - await supabase - .from('collection_pictures') - .delete() - .eq('picture_id', videoId); - - // Then add new associations + await apiClient('/api/collection-pictures', { + method: 'DELETE', + body: JSON.stringify({ picture_id: videoId, deleteAll: true }), + }); if (selectedCollections.size > 0) { - const collectionInserts = Array.from(selectedCollections).map(collectionId => ({ + const inserts = Array.from(selectedCollections).map(collectionId => ({ collection_id: collectionId, picture_id: videoId, })); - - await supabase - .from('collection_pictures') - .insert(collectionInserts); + await apiClient('/api/collection-pictures', { method: 'POST', body: JSON.stringify({ inserts }) }); } } catch (collectionError) { console.error('Collection update failed:', collectionError); toast.error(translate('Failed to update collections')); - // Don't fail the whole update if collections fail } toast.success(translate('Video updated successfully')); diff --git a/packages/ui/src/components/GlobalDragDrop.tsx b/packages/ui/src/components/GlobalDragDrop.tsx index f25a88f7..d645f312 100644 --- a/packages/ui/src/components/GlobalDragDrop.tsx +++ b/packages/ui/src/components/GlobalDragDrop.tsx @@ -4,7 +4,7 @@ import { set } from 'idb-keyval'; import { toast } from 'sonner'; import { Upload } from 'lucide-react'; import { T, translate } from '@/i18n'; -import { supabase } from '@/integrations/supabase/client'; +import { getAuthToken } from '@/lib/db'; import { useDragDrop } from '@/contexts/DragDropContext'; const GlobalDragDrop = () => { @@ -52,11 +52,9 @@ const GlobalDragDrop = () => { toast.info(translate('Processing link...')); const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL; - const { data: { session } } = await supabase.auth.getSession(); + const token = await getAuthToken(); const headers: Record = {}; - if (session?.access_token) { - headers['Authorization'] = `Bearer ${session.access_token}`; - } + if (token) headers['Authorization'] = `Bearer ${token}`; try { const response = await fetch(`${serverUrl}/api/serving/site-info?url=${encodeURIComponent(url)}`, { diff --git a/packages/ui/src/components/ImageWizard/db.ts b/packages/ui/src/components/ImageWizard/db.ts index 9a09ac8d..3bd06892 100644 --- a/packages/ui/src/components/ImageWizard/db.ts +++ b/packages/ui/src/components/ImageWizard/db.ts @@ -12,26 +12,18 @@ import { addCollectionPictures, } from "@/modules/posts/client-pictures"; import { uploadImage } from "@/lib/uploadUtils"; -import { getUserOpenAIKey } from "@/modules/user/client-user"; -import { supabase } from "@/integrations/supabase/client"; +import { getUserOpenAIKey, getUserSettings, updateUserSettings } from "@/modules/user/client-user"; // Re-export for backward compat export { getUserOpenAIKey }; /** - * Load saved wizard model from user_secrets.settings.wizard_model + * Load saved wizard model from user settings */ export const loadWizardModel = async (userId: string): Promise => { 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?.wizard_model || null; + const settings = await getUserSettings(userId); + return (settings as any)?.wizard_model || null; } catch (error) { console.error('Error loading wizard model:', error); return null; @@ -39,28 +31,11 @@ export const loadWizardModel = async (userId: string): Promise => }; /** - * Save selected model to user_secrets.settings.wizard_model + * Save selected model to user settings */ export const saveWizardModel = async (userId: string, model: string): Promise => { try { - const { data: existing } = await supabase - .from('user_secrets') - .select('settings') - .eq('user_id', userId) - .maybeSingle(); - - if (existing) { - const currentSettings = (existing.settings as Record) || {}; - const newSettings = { ...currentSettings, wizard_model: model }; - - const { error } = await supabase - .from('user_secrets') - .update({ settings: newSettings }) - .eq('user_id', userId); - - if (error) throw error; - } - // If no user_secrets row exists, skip silently (will be created when user saves API keys) + await updateUserSettings(userId, { wizard_model: model }); } catch (error) { console.error('Error saving wizard model:', error); } diff --git a/packages/ui/src/components/OrganizationsList.tsx b/packages/ui/src/components/OrganizationsList.tsx index b23b95f4..9d64f5d6 100644 --- a/packages/ui/src/components/OrganizationsList.tsx +++ b/packages/ui/src/components/OrganizationsList.tsx @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import { supabase } from "@/integrations/supabase/client"; +import { apiClient } from "@/lib/db"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Building2 } from "lucide-react"; import { Link } from "react-router-dom"; @@ -7,15 +7,7 @@ import { Link } from "react-router-dom"; const OrganizationsList = () => { const { data: organizations, isLoading } = useQuery({ queryKey: ["organizations"], - queryFn: async () => { - const { data, error } = await supabase - .from("organizations") - .select("*") - .order("created_at", { ascending: false }); - - if (error) throw error; - return data; - }, + queryFn: () => apiClient('/api/organizations'), }); if (isLoading) { diff --git a/packages/ui/src/components/PhotoGrid.tsx b/packages/ui/src/components/PhotoGrid.tsx index 7b0937f2..0883a74f 100644 --- a/packages/ui/src/components/PhotoGrid.tsx +++ b/packages/ui/src/components/PhotoGrid.tsx @@ -52,7 +52,6 @@ interface MediaGridProps { onFilesDrop?: (files: File[]) => void; showVideos?: boolean; // Toggle video display (kept for backward compatibility) sortBy?: FeedSortOption; - supabaseClient?: any; apiUrl?: string; categorySlugs?: string[]; categoryIds?: string[]; @@ -74,7 +73,6 @@ const MediaGrid = ({ onFilesDrop, showVideos = true, sortBy = 'latest', - supabaseClient, apiUrl, categorySlugs, categoryIds, @@ -117,7 +115,6 @@ const MediaGrid = ({ visibilityFilter, // Disable hook if we have custom pictures enabled: !customPictures, - supabaseClient }); // Derive loading from hook/props instead of syncing via setState diff --git a/packages/ui/src/components/PublishDialog.tsx b/packages/ui/src/components/PublishDialog.tsx index 0bad1bb1..55d3c021 100644 --- a/packages/ui/src/components/PublishDialog.tsx +++ b/packages/ui/src/components/PublishDialog.tsx @@ -6,7 +6,7 @@ import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Checkbox } from '@/components/ui/checkbox'; import { Upload, RefreshCw, GitBranch, Bookmark } from 'lucide-react'; -import { supabase } from '@/integrations/supabase/client'; +import { apiClient } from '@/lib/db'; import { useAuth } from '@/hooks/useAuth'; interface Collection { @@ -60,13 +60,7 @@ export default function PublishDialog({ setLoadingCollections(true); try { - const { data, error } = await supabase - .from('collections') - .select('id, name, slug') - .eq('user_id', user.id) - .order('name'); - - if (error) throw error; + const data = await apiClient(`/api/collections?userId=${user.id}`); setCollections(data || []); } catch (error) { console.error('Error loading collections:', error); diff --git a/packages/ui/src/components/TopNavigation.tsx b/packages/ui/src/components/TopNavigation.tsx index 4f2e134b..04288f39 100644 --- a/packages/ui/src/components/TopNavigation.tsx +++ b/packages/ui/src/components/TopNavigation.tsx @@ -21,6 +21,7 @@ const CreationWizardPopup = lazy(() => import('./CreationWizardPopup').then(m => const TopNavigation = () => { const { user, signOut, roles } = useAuth(); + console.log('user', user); const { fetchProfile, profiles } = useProfiles(); const location = useLocation(); const navigate = useNavigate(); @@ -44,7 +45,7 @@ const TopNavigation = () => { return () => unsubscribe?.(); }, []); - const authPath = '/auth'; + const authPath = '/authz'; useEffect(() => { if (user?.id) { diff --git a/packages/ui/src/components/filters/ProviderManagement.tsx b/packages/ui/src/components/filters/ProviderManagement.tsx index a25b95b2..4e629c41 100644 --- a/packages/ui/src/components/filters/ProviderManagement.tsx +++ b/packages/ui/src/components/filters/ProviderManagement.tsx @@ -5,9 +5,7 @@ */ import React, { useState, useEffect } from 'react'; -import { supabase } from '@/integrations/supabase/client'; -import { Tables, TablesInsert, TablesUpdate } from '@/integrations/supabase/types'; -import { User } from '@supabase/supabase-js'; +import { apiClient } from '@/lib/db'; import { useAuth } from '@/hooks/useAuth'; import { DEFAULT_PROVIDERS, fetchProviderModelInfo } from '@/llm/filters/providers'; import { groupModelsByCompany } from '@/llm/filters/providers/openrouter'; @@ -71,7 +69,19 @@ import { import { toast } from 'sonner'; import { Alert, AlertDescription } from '@/components/ui/alert'; -type ProviderConfig = Tables<'provider_configs'>; +type ProviderConfig = { + id: string; + user_id?: string; + name: string; + display_name: string; + base_url?: string | null; + models?: string[] | any; + rate_limits?: Record | null; + is_active?: boolean | null; + settings?: Record | null; + created_at?: string; + updated_at?: string; +}; interface ProviderSettings { apiKey?: string; @@ -109,19 +119,12 @@ export const ProviderManagement: React.FC = () => { setLoading(true); try { - // Fetch both providers and user secrets in parallel - const [providersParams, userSecrets] = await Promise.all([ - supabase - .from('provider_configs') - .select('*') - .eq('user_id', user.id) - .order('display_name', { ascending: true }), + const [loadedProvidersRaw, userSecrets] = await Promise.all([ + apiClient(`/api/provider-configs?userId=${user.id}`), getUserSecrets(user.id) ]); - if (providersParams.error) throw providersParams.error; - - let loadedProviders = providersParams.data || []; + let loadedProviders = loadedProvidersRaw || []; // Merge secrets into provider configurations for display if (userSecrets) { @@ -167,12 +170,7 @@ export const ProviderManagement: React.FC = () => { if (!deletingProvider) return; try { - const { error } = await supabase - .from('provider_configs') - .delete() - .eq('id', deletingProvider.id); - - if (error) throw error; + await apiClient(`/api/provider-configs/${deletingProvider.id}`, { method: 'DELETE' }); toast.success(`Provider "${deletingProvider.display_name}" deleted successfully`); setDeletingProvider(null); @@ -356,7 +354,7 @@ export const ProviderManagement: React.FC = () => { // Provider Edit Dialog Component interface ProviderEditDialogProps { provider: ProviderConfig | null; - user: User; + user: any; open: boolean; onOpenChange: (open: boolean) => void; onSave: () => void; @@ -569,34 +567,21 @@ const ProviderEditDialog: React.FC = ({ } // Update existing provider - const { error } = await supabase - .from('provider_configs') - .update(data) - .eq('id', provider.id); - - if (error) throw error; + await apiClient(`/api/provider-configs/${provider.id}`, { method: 'PATCH', body: JSON.stringify(data) }); toast.success('Provider updated successfully'); } else { // Create new provider with user_id - - // Handle secrets for new provider too if (formData.name === 'openai' || formData.name === 'google') { try { const secretUpdate: Record = {}; if (formData.name === 'openai') secretUpdate['openai_api_key'] = settings.apiKey || ''; if (formData.name === 'google') secretUpdate['google_api_key'] = settings.apiKey || ''; - await updateUserSecrets(user.id, secretUpdate); } catch (secretError) { console.error('Failed to update user secrets:', secretError); } } - - const { error } = await supabase - .from('provider_configs') - .insert([{ ...data, user_id: user.id }]); - - if (error) throw error; + await apiClient('/api/provider-configs', { method: 'POST', body: JSON.stringify({ ...data, user_id: user.id }) }); toast.success('Provider created successfully'); } diff --git a/packages/ui/src/components/filters/ProviderSelector.tsx b/packages/ui/src/components/filters/ProviderSelector.tsx index af5ce63e..a2957c08 100644 --- a/packages/ui/src/components/filters/ProviderSelector.tsx +++ b/packages/ui/src/components/filters/ProviderSelector.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react'; import { ProviderConfig } from '@/llm/filters/types'; -import { supabase } from '@/integrations/supabase/client'; +import { apiClient } from '@/lib/db'; import { useAuth } from '@/hooks/useAuth'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; @@ -69,18 +69,8 @@ export const ProviderSelector: React.FC = ({ setLoading(true); try { - const { data: userProviders, error } = await supabase - .from('provider_configs') - .select('*') - .eq('user_id', user.id) - .eq('is_active', true) - .order('display_name', { ascending: true }); - - if (error) { - console.error('Failed to load user providers:', error); - setProviders([]); - } else if (userProviders) { - // Convert database providers to ProviderConfig + const userProviders = await apiClient(`/api/provider-configs?userId=${user.id}&is_active=true`); + if (userProviders) { const providers = userProviders.map(dbProvider => ({ name: dbProvider.name, displayName: dbProvider.display_name, diff --git a/packages/ui/src/components/lazy-editors/AIGenerationPlugin.tsx b/packages/ui/src/components/lazy-editors/AIGenerationPlugin.tsx index 53ce06d2..84e6c5af 100644 --- a/packages/ui/src/components/lazy-editors/AIGenerationPlugin.tsx +++ b/packages/ui/src/components/lazy-editors/AIGenerationPlugin.tsx @@ -7,7 +7,7 @@ import { AIPromptPopup } from './AIPromptPopup'; import { generateText } from '@/lib/openai'; import { toast } from 'sonner'; import { getUserApiKeys as getUserSecrets } from '@/modules/user/client-user'; -import { supabase } from '@/integrations/supabase/client'; +import { apiClient } from '@/lib/db'; import { useAuth } from '@/hooks/useAuth'; import { formatTextGenPrompt } from '@/constants'; @@ -33,15 +33,9 @@ const useProviderApiKey = () => { try { console.log('Fetching API key for user:', user.id, 'provider:', provider); - const { data: userProvider, error } = await supabase - .from('provider_configs') - .select('settings') - .eq('user_id', user.id) - .eq('name', provider) - .eq('is_active', true) - .single(); - - if (error || !userProvider) return null; + const configs = await apiClient(`/api/provider-configs?userId=${user.id}&name=${provider}&is_active=true`); + const userProvider = configs?.[0]; + if (!userProvider) return null; const settings = userProvider.settings as any; return settings?.apiKey || null; } catch (error) { diff --git a/packages/ui/src/components/widgets/ImagePickerDialog.tsx b/packages/ui/src/components/widgets/ImagePickerDialog.tsx index e8808ea2..2f07504f 100644 --- a/packages/ui/src/components/widgets/ImagePickerDialog.tsx +++ b/packages/ui/src/components/widgets/ImagePickerDialog.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'; -import { supabase } from '@/integrations/supabase/client'; // Still needed for collections (no API yet) +import { apiClient } from '@/lib/db'; import { fetchPictures as fetchPicturesAPI, fetchUserPictures } from '@/modules/posts/client-pictures'; import { fetchPostsList } from '@/modules/posts/client-posts'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; @@ -349,13 +349,7 @@ export const ImagePickerDialog: React.FC = ({ if (!user) return; try { - const { data, error } = await supabase - .from('collections') - .select('id, name, slug') - .eq('user_id', user.id) - .order('name'); - - if (error) throw error; + const data = await apiClient(`/api/collections?userId=${user.id}`); setCollections(data || []); } catch (error) { console.error('Error fetching collections:', error); @@ -390,13 +384,8 @@ export const ImagePickerDialog: React.FC = ({ } try { - const { data, error } = await supabase - .from('collection_pictures') - .select('picture_id') - .in('collection_id', selectedCollections); - - if (error) throw error; - + const params = selectedCollections.map(id => `collectionId=${id}`).join('&'); + const data = await apiClient<{ picture_id: string }[]>(`/api/collection-pictures?${params}`); const pictureIdsInCollections = new Set(data?.map(cp => cp.picture_id) || []); setFinalPictures(filteredPictures.filter(pic => pictureIdsInCollections.has(pic.id))); } catch (error) { @@ -434,13 +423,8 @@ export const ImagePickerDialog: React.FC = ({ onMultiSelectPictures(allPics); } } else { - console.log('Selected ID:', selectedId); - console.log('Selected Post ID:', selectedPostId); - if (selectedId) { - if (onSelect) onSelect(selectedId); - if (onSelectPicture) { let pic = pictures.find(p => p.id === selectedId); // If not found in pictures (e.g. selected via posts mode), try the post's cover diff --git a/packages/ui/src/hooks/useAuth.tsx b/packages/ui/src/hooks/useAuth.tsx index 7b2c2661..bdf832a9 100644 --- a/packages/ui/src/hooks/useAuth.tsx +++ b/packages/ui/src/hooks/useAuth.tsx @@ -1,11 +1,14 @@ import { createContext, useContext, useEffect, useState } from 'react'; import { useAuth as useOidcAuth } from 'react-oidc-context'; -import { fetchUserRoles } from '@/modules/user/client-user'; +import { fetchUserIdentity } from '@/modules/user/client-user'; // ─── Types ──────────────────────────────────────────────────────────────────── export interface AuthUser { - id: string; // Zitadel numeric sub + /** Resolved app UUID (profiles.user_id) — use this for all API calls and routing */ + id: string; + /** Original Zitadel numeric sub from the OIDC token */ + sub: string; email: string | undefined; user_metadata: { display_name?: string; @@ -23,7 +26,7 @@ interface AuthContextType { roles: string[]; loading: boolean; isAuthenticated: boolean; - signIn: () => Promise; + signIn: (redirectTo?: string) => Promise; signOut: () => Promise; /** @deprecated Use signIn() — redirects to Zitadel */ signUp: (...args: any[]) => Promise<{ data: null; error: Error }>; @@ -42,31 +45,41 @@ const AuthContext = createContext(undefined); export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const oidcAuth = useOidcAuth(); const [roles, setRoles] = useState([]); - const [rolesLoading, setRolesLoading] = useState(false); + const [identityLoading, setIdentityLoading] = useState(false); + // Resolved app UUID — starts as null until the /api/me/identity call completes + const [resolvedId, setResolvedId] = useState(null); useEffect(() => { if (oidcAuth.isLoading) return; if (!oidcAuth.isAuthenticated || !oidcAuth.user) { setRoles([]); + setResolvedId(null); return; } - const sub = oidcAuth.user.profile.sub; - - setRolesLoading(true); - fetchUserRoles(sub) - .then(r => setRoles(r as string[])) - .catch(err => { - console.error('[AuthProvider] Failed to fetch roles:', err); - setRoles([]); + setIdentityLoading(true); + fetchUserIdentity() + .then(({ id, roles: r }) => { + setResolvedId(id); + setRoles(r); }) - .finally(() => setRolesLoading(false)); + .catch(err => { + console.error('[AuthProvider] Failed to fetch identity:', err); + setRoles([]); + // Fall back to the raw numeric sub so the app isn't completely broken + setResolvedId(null); + }) + .finally(() => setIdentityLoading(false)); }, [oidcAuth.isAuthenticated, oidcAuth.isLoading]); + const oidcSub = oidcAuth.user?.profile.sub ?? ''; + const user: AuthUser | null = oidcAuth.user ? { - id: oidcAuth.user.profile.sub, + // Use the server-resolved UUID; fall back to the OIDC sub while loading + id: resolvedId ?? oidcSub, + sub: oidcSub, email: oidcAuth.user.profile.email, user_metadata: { display_name: oidcAuth.user.profile.name, @@ -84,22 +97,16 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { user, session: null, roles, - loading: oidcAuth.isLoading || rolesLoading, + loading: oidcAuth.isLoading || identityLoading, isAuthenticated: oidcAuth.isAuthenticated, - signIn: () => oidcAuth.signinRedirect(), - signOut: () => oidcAuth.signoutRedirect(), + signIn: (redirectTo = '/') => oidcAuth.signinRedirect({ state: { redirectTo } }), + signOut: () => oidcAuth.signoutRedirect({ post_logout_redirect_uri: window.location.origin + '/' }), signUp: async () => ({ data: null, error: new Error('Sign up via Zitadel — use the /authz page'), }), - signInWithGithub: async () => { - await oidcAuth.signinRedirect(); - return { error: null }; - }, - signInWithGoogle: async () => { - await oidcAuth.signinRedirect(); - return { error: null }; - }, + signInWithGithub: async () => { await oidcAuth.signinRedirect(); return { error: null }; }, + signInWithGoogle: async () => { await oidcAuth.signinRedirect(); return { error: null }; }, resetPassword: async () => ({ error: new Error('Reset password via the Zitadel portal'), }), diff --git a/packages/ui/src/hooks/useFeedData.ts b/packages/ui/src/hooks/useFeedData.ts index 3d13cb4b..f5e19118 100644 --- a/packages/ui/src/hooks/useFeedData.ts +++ b/packages/ui/src/hooks/useFeedData.ts @@ -8,8 +8,6 @@ import { getCurrentLang } from '@/i18n'; import { fetchWithDeduplication, getAuthToken } from '@/lib/db'; import { fetchFeed } from '@/modules/feed/client-feed'; -const { supabase } = await import('@/integrations/supabase/client'); - export type FeedSortOption = 'latest' | 'top'; interface UseFeedDataProps { @@ -19,7 +17,6 @@ interface UseFeedDataProps { orgSlug?: string; enabled?: boolean; sortBy?: FeedSortOption; - supabaseClient?: any; categoryIds?: string[]; categorySlugs?: string[]; contentType?: 'posts' | 'pages' | 'pictures' | 'files' | 'places'; @@ -33,7 +30,6 @@ export const useFeedData = ({ orgSlug, enabled = true, sortBy = 'latest', - supabaseClient, categoryIds, categorySlugs, contentType, @@ -232,7 +228,7 @@ export const useFeedData = ({ setLoading(false); setIsFetchingMore(false); } - }, [source, sourceId, isOrgContext, orgSlug, enabled, fetchProfiles, sortBy, supabaseClient, categoryIds, categorySlugs, contentType, visibilityFilter]); + }, [source, sourceId, isOrgContext, orgSlug, enabled, fetchProfiles, sortBy, categoryIds, categorySlugs, contentType, visibilityFilter]); // Initial Load useEffect(() => { diff --git a/packages/ui/src/hooks/usePlaygroundLogic.tsx b/packages/ui/src/hooks/usePlaygroundLogic.tsx index 7dc07b83..b2a4fd2b 100644 --- a/packages/ui/src/hooks/usePlaygroundLogic.tsx +++ b/packages/ui/src/hooks/usePlaygroundLogic.tsx @@ -4,7 +4,7 @@ import { toast } from 'sonner'; import { useWidgetLoader } from './useWidgetLoader.tsx'; import { useLayouts } from '@/modules/layout/useLayouts'; import { Database } from '@/integrations/supabase/types'; -import { supabase } from "@/integrations/supabase/client"; +import { apiClient } from "@/lib/db"; type Layout = Database['public']['Tables']['layouts']['Row']; type LayoutVisibility = Database['public']['Enums']['layout_visibility']; @@ -420,30 +420,14 @@ export function usePlaygroundLogic() { toast.info("Sending test email..."); - const { data: sessionData } = await supabase.auth.getSession(); - const token = sessionData.session?.access_token; - - const headers: HeadersInit = { - 'Content-Type': 'application/json' - }; - if (token) headers['Authorization'] = `Bearer ${token}`; - - const response = await fetch(`${serverUrl}/api/send/email/${dummyId}`, { + await apiClient(`/api/send/email/${dummyId}`, { method: 'POST', - headers, body: JSON.stringify({ html, subject: `[Test] ${layout.name} - ${new Date().toLocaleTimeString()}` }) }); - - if (response.ok) { - toast.success("Test email sent!"); - } else { - const err = await response.text(); - console.error("Failed to send test email", err); - toast.error(`Failed to send: ${response.statusText}`); - } + toast.success("Test email sent!"); } catch (e) { console.error("Failed to send test email", e); diff --git a/packages/ui/src/hooks/useProviderSettings.ts b/packages/ui/src/hooks/useProviderSettings.ts index d5fcaa7a..49fd6e7d 100644 --- a/packages/ui/src/hooks/useProviderSettings.ts +++ b/packages/ui/src/hooks/useProviderSettings.ts @@ -1,9 +1,17 @@ import { useState, useEffect } from 'react'; -import { supabase } from '@/integrations/supabase/client'; import { useAuth } from '@/hooks/useAuth'; -import { Tables } from '@/integrations/supabase/types'; +import { apiClient } from '@/lib/db'; -export type ProviderConfig = Tables<'provider_configs'>; +export type ProviderConfig = { + name: string; + display_name: string; + base_url?: string; + models?: string[]; + rate_limits?: Record; + is_active?: boolean; + settings?: Record; + user_id?: string; +}; const STORAGE_KEY = 'provider-settings'; @@ -34,14 +42,7 @@ export const useProviderSettings = () => { setLoading(true); try { - const { data, error } = await supabase - .from('provider_configs') - .select('*') - .eq('user_id', user.id) - .eq('is_active', true) - .order('display_name'); - - if (error) throw error; + const data = await apiClient(`/api/provider-configs?userId=${user.id}&is_active=true`); setProviders(data || []); diff --git a/packages/ui/src/image-api.ts b/packages/ui/src/image-api.ts index 5db3fad1..a4ee4ff2 100644 --- a/packages/ui/src/image-api.ts +++ b/packages/ui/src/image-api.ts @@ -1,5 +1,5 @@ import { GoogleGenerativeAI, Part, GenerationConfig } from "@google/generative-ai"; -import { supabase } from "@/integrations/supabase/client"; +import { userManager } from "@/lib/oidc"; import { getUserGoogleApiKey } from "./modules/user/client-user"; // Simple logger for user feedback (safety messages) @@ -39,13 +39,16 @@ interface ImageResult { // Get user's Google API key from user_secrets export const getGoogleApiKey = async (): Promise => { try { - const { data: { user } } = await supabase.auth.getUser(); - if (!user) { + const oidcUser = await userManager.getUser(); + const userId = oidcUser?.profile.sub + ? (process.env.VITE_ZITADEL_ADMIN_UUID || oidcUser.profile.sub) + : null; + if (!userId) { logger.error('No authenticated user found'); return null; } - const apiKey = await getUserGoogleApiKey(user.id); + const apiKey = await getUserGoogleApiKey(userId); if (!apiKey) { logger.error('No Google API key found in user secrets. Please add your Google API key in your profile settings.'); diff --git a/packages/ui/src/lib/aimlapi.ts b/packages/ui/src/lib/aimlapi.ts index aff475d3..532ceae9 100644 --- a/packages/ui/src/lib/aimlapi.ts +++ b/packages/ui/src/lib/aimlapi.ts @@ -1,4 +1,4 @@ -import { supabase } from "@/integrations/supabase/client"; +import { apiClient } from "@/lib/db"; // Simple logger for user feedback const logger = { @@ -10,32 +10,16 @@ const logger = { const AIMLAPI_BASE_URL = 'https://api.aimlapi.com'; -// Get user's AIML API key from their profile +// Get user's AIML API key from server secrets const getAimlApiKey = async (): Promise => { try { - const { data: { user } } = await supabase.auth.getUser(); - if (!user) { - logger.error('No authenticated user found'); + const data = await apiClient<{ api_keys?: Record }>('/api/me/secrets'); + const key = data.api_keys?.aimlapi_api_key; + if (!key) { + logger.error('No AIML API key found. Please add your AIML API key in your profile settings.'); return null; } - - const { data: profile, error } = await supabase - .from('profiles') - .select('aimlapi_api_key') - .eq('user_id', user.id) - .single(); - - if (error) { - logger.error('Error fetching user profile:', error); - return null; - } - - if (!profile?.aimlapi_api_key) { - logger.error('No AIML API key found in user profile. Please add your AIML API key in your profile settings.'); - return null; - } - - return profile.aimlapi_api_key; + return key; } catch (error) { logger.error('Error getting AIML API key:', error); return null; diff --git a/packages/ui/src/lib/bria.ts b/packages/ui/src/lib/bria.ts index 2ecce585..d3b6c21d 100644 --- a/packages/ui/src/lib/bria.ts +++ b/packages/ui/src/lib/bria.ts @@ -1,4 +1,4 @@ -import { supabase } from "@/integrations/supabase/client"; +import { apiClient } from "@/lib/db"; // Simple logger for user feedback const logger = { @@ -10,32 +10,16 @@ const logger = { const BRIA_BASE_URL = 'https://engine.prod.bria-api.com/v1'; -// Get user's Bria API key from their profile +// Get user's Bria API key from server secrets const getBriaApiKey = async (): Promise => { try { - const { data: { user } } = await supabase.auth.getUser(); - if (!user) { - logger.error('No authenticated user found'); + const data = await apiClient<{ api_keys?: Record }>('/api/me/secrets'); + const key = data.api_keys?.bria_api_key; + if (!key) { + logger.error('No Bria API key found. Please add your Bria API key in your profile settings.'); return null; } - - const { data: profile, error } = await supabase - .from('profiles') - .select('bria_api_key') - .eq('user_id', user.id) - .single(); - - if (error) { - logger.error('Error fetching user profile:', error); - return null; - } - - if (!profile?.bria_api_key) { - logger.error('No Bria API key found in user profile. Please add your Bria API key in your profile settings.'); - return null; - } - - return profile.bria_api_key; + return key; } catch (error) { logger.error('Error getting Bria API key:', error); return null; diff --git a/packages/ui/src/lib/db.ts b/packages/ui/src/lib/db.ts index 3e844eb3..610dfc23 100644 --- a/packages/ui/src/lib/db.ts +++ b/packages/ui/src/lib/db.ts @@ -1,9 +1,6 @@ import { queryClient } from './queryClient'; -import { supabase as defaultSupabase } from "@/integrations/supabase/client"; import { userManager } from './oidc'; -import { SupabaseClient } from "@supabase/supabase-js"; - // Deprecated: Caching now handled by React Query // Keeping for backward compatibility type CacheStorageType = 'memory' | 'local'; @@ -149,17 +146,13 @@ export const invalidateServerCache = async (types: string[]) => { -export const checkLikeStatus = async (userId: string, pictureId: string, client?: SupabaseClient) => { - const supabase = client || defaultSupabase; +export const checkLikeStatus = async (userId: string, pictureId: string) => { return fetchWithDeduplication(`like-${userId}-${pictureId}`, async () => { - const { data, error } = await supabase - .from('likes') - .select('id') - .eq('user_id', userId) - .eq('picture_id', pictureId) - .maybeSingle(); - - if (error) throw error; - return !!data; + try { + const data = await apiClient<{ liked: boolean }>(`/api/pictures/${pictureId}/like-status?userId=${userId}`); + return data.liked; + } catch { + return false; + } }); }; diff --git a/packages/ui/src/lib/oidc.ts b/packages/ui/src/lib/oidc.ts index be456cf3..daab36df 100644 --- a/packages/ui/src/lib/oidc.ts +++ b/packages/ui/src/lib/oidc.ts @@ -7,7 +7,7 @@ export const userManager = new UserManager({ authority, client_id, redirect_uri: window.location.origin + '/authz', - post_logout_redirect_uri: window.location.origin + '/authz', + post_logout_redirect_uri: window.location.origin + '/', response_type: 'code', scope: 'openid profile email', loadUserInfo: true, diff --git a/packages/ui/src/lib/openai.ts b/packages/ui/src/lib/openai.ts index 45dc8de3..4b24ab82 100644 --- a/packages/ui/src/lib/openai.ts +++ b/packages/ui/src/lib/openai.ts @@ -10,7 +10,7 @@ * See PRESET_TOOLS mapping below for tool combinations. */ import OpenAI from 'openai'; -import { supabase } from "@/integrations/supabase/client"; +import { getAuthToken as getZitadelToken } from "@/lib/db"; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { RunnableToolFunctionWithParse } from 'openai/lib/RunnableFunction'; @@ -80,12 +80,12 @@ const PRESET_TOOLS: Record RunnableToolFunction // Get user's session token for proxy authentication const getAuthToken = async (): Promise => { try { - const { data: { session } } = await supabase.auth.getSession(); - if (!session?.access_token) { + const token = await getZitadelToken(); + if (!token) { consoleLogger.error('No authenticated session found'); return null; } - return session.access_token; + return token; } catch (error) { consoleLogger.error('Error getting auth token:', error); return null; @@ -1281,8 +1281,7 @@ export const runTools = async (options: RunToolsOptions): Promise console.error(`[ERROR] ${message}`, data), }; -// Call the edge function to proxy Replicate API requests +// Call the server-side Replicate proxy const callReplicateProxy = async (model: string, input: any): Promise => { - const { data: { session } } = await supabase.auth.getSession(); - if (!session) { - throw new Error('Not authenticated'); - } + const token = await getAuthToken(); + if (!token) throw new Error('Not authenticated'); - const SUPABASE_URL = "https://ytoadlpbdguriiccjnip.supabase.co"; - - const response = await fetch(`${SUPABASE_URL}/functions/v1/replicate-proxy`, { + const response = await fetch(`${serverUrl}/api/replicate/run`, { method: 'POST', headers: { - 'Authorization': `Bearer ${session.access_token}`, + 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, - body: JSON.stringify({ - action: 'run', - model, - input, - }), + body: JSON.stringify({ model, input }), }); if (!response.ok) { diff --git a/packages/ui/src/lib/uploadUtils.ts b/packages/ui/src/lib/uploadUtils.ts index c125665a..fad7922d 100644 --- a/packages/ui/src/lib/uploadUtils.ts +++ b/packages/ui/src/lib/uploadUtils.ts @@ -1,5 +1,4 @@ -import { supabase } from '@/integrations/supabase/client'; -import { getAuthToken, serverUrl } from './db'; +import { getAuthToken, serverUrl, apiClient } from './db'; /** * Uploads an image file via the server API. @@ -57,24 +56,20 @@ export const uploadGeneratedImageData = async ( }; /** - * Creates a picture record in the database. + * Creates a picture record in the database via API. */ export const createPictureRecord = async (userId: string, file: File, publicUrl: string, meta?: any) => { - const { data, error } = await supabase - .from('pictures') - .insert({ + return apiClient('/api/pictures', { + method: 'POST', + body: JSON.stringify({ user_id: userId, title: file.name.split('.')[0] || 'Uploaded Image', description: null, image_url: publicUrl, - type: 'supabase-image', + type: 'image', meta: meta || {}, - }) - .select() - .single(); - - if (error) throw error; - return data; + }), + }); }; /** diff --git a/packages/ui/src/llm/filters/providers.ts b/packages/ui/src/llm/filters/providers.ts index b1753e62..41157c7a 100644 --- a/packages/ui/src/llm/filters/providers.ts +++ b/packages/ui/src/llm/filters/providers.ts @@ -5,8 +5,7 @@ import { ProviderConfig } from './types'; import { generateText } from '@/lib/openai'; -import { supabase } from '@/integrations/supabase/client'; -import { Tables } from '@/integrations/supabase/types'; +import { apiClient } from '@/lib/db'; import { fetchOpenRouterModelInfo } from './providers/openrouter'; import { fetchOpenAIModelInfo } from './providers/openai'; @@ -66,26 +65,18 @@ export const getProviderConfig = (providerName: string): ProviderConfig | null = }; /** - * Get all available providers from database or defaults + * Get all available providers from API or defaults */ export const getAvailableProviders = async (): Promise => { try { - const { data, error } = await supabase - .from('provider_configs') - .select('*') - .eq('is_active', true); - - if (error) throw error; - + const data = await apiClient('/api/provider-configs?is_active=true'); if (data && data.length > 0) { - // Convert database format to ProviderConfig format return data.map(dbProvider => convertDbToConfig(dbProvider)); } } catch (error) { - console.error('Failed to load providers from database, using defaults:', error); + console.error('Failed to load providers from API, using defaults:', error); } - // Fallback to defaults return Object.values(DEFAULT_PROVIDERS).filter(provider => provider.isActive); }; @@ -100,7 +91,7 @@ export const getAvailableProvidersSync = (): ProviderConfig[] => { /** * Convert database provider format to ProviderConfig */ -const convertDbToConfig = (dbProvider: Tables<'provider_configs'>): ProviderConfig => { +const convertDbToConfig = (dbProvider: any): ProviderConfig => { return { name: dbProvider.name, displayName: dbProvider.display_name, diff --git a/packages/ui/src/llm/filters/providers/openai.ts b/packages/ui/src/llm/filters/providers/openai.ts index 80716dbd..72e8844c 100644 --- a/packages/ui/src/llm/filters/providers/openai.ts +++ b/packages/ui/src/llm/filters/providers/openai.ts @@ -4,7 +4,7 @@ */ import OpenAI from 'openai'; -import { supabase } from '@/integrations/supabase/client'; +import { apiClient } from '@/lib/db'; // Types based on ref/openai.ts interface OpenAIModel { @@ -46,32 +46,13 @@ const getOpenAIApiKey = async (providedKey?: string): Promise => } try { - const { data: { user } } = await supabase.auth.getUser(); - if (!user) { - console.error('No authenticated user found'); - return null; - } - - const { data: secretData, error } = await supabase - .from('user_secrets') - .select('settings') - .eq('user_id', user.id) - .maybeSingle(); - - if (error) { - console.error('Error fetching user secrets:', error); - return null; - } - - const settings = secretData?.settings as { api_keys?: Record } | null; - const apiKey = settings?.api_keys?.openai_api_key; - - if (!apiKey) { + const data = await apiClient<{ api_keys?: Record }>('/api/me/secrets'); + const key = data.api_keys?.openai_api_key; + if (!key) { console.error('No OpenAI API key found in user secrets. Please add your OpenAI API key in your profile settings.'); return null; } - - return apiKey; + return key; } catch (error) { console.error('Error getting OpenAI API key:', error); return null; diff --git a/packages/ui/src/modules/analytics/AnalyticsDashboard.tsx b/packages/ui/src/modules/analytics/AnalyticsDashboard.tsx index dd7068c1..9de60e87 100644 --- a/packages/ui/src/modules/analytics/AnalyticsDashboard.tsx +++ b/packages/ui/src/modules/analytics/AnalyticsDashboard.tsx @@ -33,7 +33,7 @@ import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { fetchAnalytics, clearAnalytics, subscribeToAnalyticsStream } from '@/modules/analytics/client-analytics'; -import { supabase } from '@/integrations/supabase/client'; +import { apiClient } from '@/lib/db'; import { filterModelToParams, paramsToFilterModel, @@ -372,23 +372,12 @@ const AnalyticsDashboard = () => { if (!confirm(`Ban ${ips.length} IP(s)? They will be blocked from all requests.`)) return; setBanning(true); try { - const { data: { session } } = await supabase.auth.getSession(); - const baseUrl = serverUrl || import.meta.env.VITE_SERVER_IMAGE_API_URL || ''; - const res = await fetch(`${baseUrl}/api/admin/bans/ban-ip`, { + const result = await apiClient<{ banned: number; message: string }>('/api/admin/bans/ban-ip', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${session?.access_token || ''}` - }, - body: JSON.stringify({ ips }) + body: JSON.stringify({ ips }), }); - const result = await res.json(); - if (res.ok) { - toast.success(`${result.banned} IP(s) banned`, { description: result.message }); - setSelectedUserIps(EMPTY_SELECTION); - } else { - toast.error('Ban failed', { description: result.message || res.statusText }); - } + toast.success(`${result.banned} IP(s) banned`, { description: result.message }); + setSelectedUserIps(EMPTY_SELECTION); } catch (err: any) { toast.error('Ban failed', { description: err.message }); } finally { diff --git a/packages/ui/src/modules/campaigns/client-campaigns.ts b/packages/ui/src/modules/campaigns/client-campaigns.ts index 9208ff76..67a9556b 100644 --- a/packages/ui/src/modules/campaigns/client-campaigns.ts +++ b/packages/ui/src/modules/campaigns/client-campaigns.ts @@ -1,5 +1,3 @@ -import { supabase as defaultSupabase } from "@/integrations/supabase/client"; - // ─── Types ──────────────────────────────────────────────────────────────────── export interface Campaign { diff --git a/packages/ui/src/modules/categories/client-categories.ts b/packages/ui/src/modules/categories/client-categories.ts index 965c30c9..7d36c056 100644 --- a/packages/ui/src/modules/categories/client-categories.ts +++ b/packages/ui/src/modules/categories/client-categories.ts @@ -31,8 +31,7 @@ export const fetchCategories = async (options?: { parentSlug?: string; includeCh const cacheKey = parts.join('-'); return fetchWithDeduplication(cacheKey, async () => { - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = {}; if (token) headers['Authorization'] = `Bearer ${token}`; diff --git a/packages/ui/src/modules/contacts/client-contacts.ts b/packages/ui/src/modules/contacts/client-contacts.ts index 2cbd37fe..f798c2b4 100644 --- a/packages/ui/src/modules/contacts/client-contacts.ts +++ b/packages/ui/src/modules/contacts/client-contacts.ts @@ -1,4 +1,3 @@ -import { supabase as defaultSupabase } from "@/integrations/supabase/client"; import { fetchWithDeduplication } from "@/lib/db"; // ─── Types ──────────────────────────────────────────────────────────────────── diff --git a/packages/ui/src/modules/contacts/client-mailboxes.ts b/packages/ui/src/modules/contacts/client-mailboxes.ts index cc0d5310..9c040ca4 100644 --- a/packages/ui/src/modules/contacts/client-mailboxes.ts +++ b/packages/ui/src/modules/contacts/client-mailboxes.ts @@ -3,8 +3,6 @@ * Calls server routes at /api/contacts/mailboxes */ -import { supabase as defaultSupabase } from "@/integrations/supabase/client"; - const SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || ''; const API_BASE = '/api/contacts/mailboxes'; diff --git a/packages/ui/src/modules/i18n/client-i18n.ts b/packages/ui/src/modules/i18n/client-i18n.ts index f44f4b17..a629894a 100644 --- a/packages/ui/src/modules/i18n/client-i18n.ts +++ b/packages/ui/src/modules/i18n/client-i18n.ts @@ -49,8 +49,7 @@ export const translateText = async (text: string, srcLang: string, dstLang: stri export const fetchGlossaries = async () => { // GET /api/i18n/glossaries return fetchWithDeduplication('i18n-glossaries', async () => { - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = {}; if (token) headers['Authorization'] = `Bearer ${token}`; diff --git a/packages/ui/src/modules/layout/GenericCanvasEdit.tsx b/packages/ui/src/modules/layout/GenericCanvasEdit.tsx index 3ee0f1ce..8bb5ffb2 100644 --- a/packages/ui/src/modules/layout/GenericCanvasEdit.tsx +++ b/packages/ui/src/modules/layout/GenericCanvasEdit.tsx @@ -7,7 +7,7 @@ import FlexibleContainerRenderer from './FlexibleContainerRenderer'; import { WidgetPalette } from './WidgetPalette'; import { uploadAndCreatePicture } from '@/lib/uploadUtils'; -import { supabase } from '@/integrations/supabase/client'; +import { useAuth } from '@/hooks/useAuth'; import { toast } from 'sonner'; import { translate } from '@/i18n'; @@ -52,6 +52,7 @@ const GenericCanvasEdit: React.FC = ({ onSave, selectionBreadcrumb }) => { + const { user } = useAuth(); const { loadedPages, loadPageLayout, @@ -165,7 +166,6 @@ const GenericCanvasEdit: React.FC = ({ const toastId = toast.loading(translate('Uploading images...')); try { - const { data: { user } } = await supabase.auth.getUser(); if (!user) { toast.error(translate('You must be logged in to upload images'), { id: toastId }); return; diff --git a/packages/ui/src/modules/pages/UserPage.tsx b/packages/ui/src/modules/pages/UserPage.tsx index 1457e597..6891c82e 100644 --- a/packages/ui/src/modules/pages/UserPage.tsx +++ b/packages/ui/src/modules/pages/UserPage.tsx @@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button"; import { PanelLeftClose, PanelLeftOpen } from "lucide-react"; import { T, translate } from "@/i18n"; +import { apiClient } from "@/lib/db"; import { mergePageVariables } from "@/lib/page-variables"; import { useAppStore } from "@/store/appStore"; @@ -185,16 +186,10 @@ const UserPageContent = ({ userId: propUserId, slug: propSlug, embedded = false, // Fetch original (no lang) for the editor to avoid saving translations as source. const lang = getCurrentLang(); if (lang && lang !== srcLang) { - const { supabase: defaultSupabase } = await import('@/integrations/supabase/client'); - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; - const headers: HeadersInit = {}; - if (token) headers['Authorization'] = `Bearer ${token}`; - const res = await fetch(`/api/user-page/${id}/${slugStr}`, { headers }); - if (res.ok) { - const orig = await res.json(); + try { + const orig = await apiClient(`/api/user-page/${id}/${slugStr}`); setOriginalPage(orig.page); - } else { + } catch { setOriginalPage(data.page); // fallback } } else { diff --git a/packages/ui/src/modules/pages/editor/ribbons/PageRibbonBar.tsx b/packages/ui/src/modules/pages/editor/ribbons/PageRibbonBar.tsx index 49160108..915c9b48 100644 --- a/packages/ui/src/modules/pages/editor/ribbons/PageRibbonBar.tsx +++ b/packages/ui/src/modules/pages/editor/ribbons/PageRibbonBar.tsx @@ -3,7 +3,7 @@ import { useDraggable } from '@dnd-kit/core'; import { useNavigate, useParams } from 'react-router-dom'; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; -import { supabase } from "@/integrations/supabase/client"; +import { getAuthToken } from "@/lib/db"; import { toast } from "sonner"; import { LayoutTemplate, @@ -452,8 +452,7 @@ export const PageRibbonBar = ({ const fetchSize = async () => { setLoadingSize(true); try { - const session = await supabase.auth.getSession(); - const token = session.data.session?.access_token; + const token = await getAuthToken(); // Note: This endpoint must match the iframe preview endpoint const endpoint = `${baseUrl}/user/${page.owner}/pages/${page.slug}/email-preview`; diff --git a/packages/ui/src/modules/places/gridsearch/GridSearchStreamContext.tsx b/packages/ui/src/modules/places/gridsearch/GridSearchStreamContext.tsx index dc9a2849..2c71cd64 100644 --- a/packages/ui/src/modules/places/gridsearch/GridSearchStreamContext.tsx +++ b/packages/ui/src/modules/places/gridsearch/GridSearchStreamContext.tsx @@ -1,6 +1,6 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import { getPlacesGridSearchStreamUrl } from '../client-gridsearch'; -import { supabase } from '@/integrations/supabase/client'; +import { getAuthToken } from '@/lib/db'; import { type LogEntry } from '@/contexts/LogContext'; export type StreamPhase = 'idle' | 'grid' | 'searching' | 'enriching' | 'complete' | 'failed'; @@ -317,8 +317,8 @@ export function GridSearchStreamProvider({ const connect = async () => { try { - const { data: session } = await supabase.auth.getSession(); - const url = getPlacesGridSearchStreamUrl(jobId, session.session?.access_token); + const token = await getAuthToken(); + const url = getPlacesGridSearchStreamUrl(jobId, token); es = new EventSource(url); setState(prev => ({ ...prev, streaming: true, connected: true })); diff --git a/packages/ui/src/modules/places/usePlacesSettings.ts b/packages/ui/src/modules/places/usePlacesSettings.ts index 765b33d6..4fb3916d 100644 --- a/packages/ui/src/modules/places/usePlacesSettings.ts +++ b/packages/ui/src/modules/places/usePlacesSettings.ts @@ -1,8 +1,8 @@ import { useState, useEffect, useCallback } from 'react'; import { useAuth } from '@/hooks/useAuth'; import { toast } from 'sonner'; -import { supabase } from '@/integrations/supabase/client'; import { translate } from '@/i18n'; +import { getUserSettings, updateUserSettings } from '@/modules/user/client-user'; export interface PlacesSettings { known_types: string[]; @@ -19,15 +19,7 @@ export const usePlacesSettings = () => { if (!userId) return; setLoading(true); try { - const { data, error } = await supabase - .from('profiles') - .select('settings') - .eq('user_id', userId) - .maybeSingle(); - - if (error) throw error; - - const fetchedSettings = (data?.settings as Record) || {}; + const fetchedSettings = (await getUserSettings(userId) as Record) || {}; setSettings({ known_types: Array.isArray(fetchedSettings.known_types) ? fetchedSettings.known_types : [], excluded_types: Array.isArray(fetchedSettings.excluded_types) ? fetchedSettings.excluded_types : [], @@ -43,42 +35,13 @@ export const usePlacesSettings = () => { const updateExcludedTypes = async (newExcludedTypes: string[]) => { if (!user) return; try { - // Optimistic update setSettings(prev => ({ ...prev, excluded_types: newExcludedTypes })); - - // Re-fetch to ensure we have latest known_types before merging - const { data: currentProfile, error: fetchError } = await supabase - .from('profiles') - .select('settings') - .eq('user_id', user.id) - .maybeSingle(); - - if (fetchError) { - console.error('Error fetching settings for update:', fetchError); - throw fetchError; - } - - // Preservation check: If we found no profile, but we expect one (e.g. existing user), this is risky. - // However, maybeSingle returns null for new users. - - const currentSettings = (currentProfile?.settings as Record) || {}; - - const { error } = await supabase - .from('profiles') - .upsert({ - user_id: user.id, - settings: { - ...currentSettings, - excluded_types: newExcludedTypes, - }, - }, { onConflict: 'user_id' }); - - if (error) throw error; + const currentSettings = (await getUserSettings(user.id) as Record) || {}; + await updateUserSettings(user.id, { ...currentSettings, excluded_types: newExcludedTypes }); toast.success(translate('Settings saved')); } catch (error: any) { console.error('Error updating settings:', error); toast.error(translate('Failed to save settings')); - // Revert on error fetchSettings(); } }; diff --git a/packages/ui/src/modules/posts/NewPost.tsx b/packages/ui/src/modules/posts/NewPost.tsx index 37c7c27e..4c5e4e68 100644 --- a/packages/ui/src/modules/posts/NewPost.tsx +++ b/packages/ui/src/modules/posts/NewPost.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { supabase } from "@/integrations/supabase/client"; +import { getAuthToken } from "@/lib/db"; import { useAuth } from "@/hooks/useAuth"; import { useNavigate, Navigate } from "react-router-dom"; import { CreationWizardPopup } from "@/components/CreationWizardPopup"; @@ -31,11 +31,9 @@ const NewPost = () => { const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || ''; addLog(`Fetching site info from: ${serverUrl || '/api'}/serving/site-info`); - const { data: { session } } = await supabase.auth.getSession(); + const token = await getAuthToken(); const headers: Record = {}; - if (session?.access_token) { - headers['Authorization'] = `Bearer ${session.access_token}`; - } + if (token) headers['Authorization'] = `Bearer ${token}`; // Ensure we have a valid URL protocol let targetUrl = url; diff --git a/packages/ui/src/modules/posts/client-pictures.ts b/packages/ui/src/modules/posts/client-pictures.ts index 803fac10..04de89a1 100644 --- a/packages/ui/src/modules/posts/client-pictures.ts +++ b/packages/ui/src/modules/posts/client-pictures.ts @@ -1,6 +1,5 @@ import { PostMediaItem } from "@/modules/posts/views/types"; import { MediaItem } from "@/types"; -import { SupabaseClient } from "@supabase/supabase-js"; import { fetchWithDeduplication, apiClient, getAuthHeaders } from "@/lib/db"; import { uploadImage } from "@/lib/uploadUtils"; import { FetchMediaOptions } from "@/utils/mediaUtils"; @@ -26,32 +25,32 @@ import { FetchMediaOptions } from "@/utils/mediaUtils"; "widgetId": "photo-card" } */ -export const fetchVersions = async (mediaItem: PostMediaItem, userId?: string, client?: SupabaseClient) => { +export const fetchVersions = async (mediaItem: PostMediaItem, userId?: string) => { const key = `versions-${mediaItem.id}-${userId || 'anon'}`; return fetchWithDeduplication(key, async () => { const rootId = mediaItem.id; // Server resolves full parent chain via collectVersionTree - const params = new URLSearchParams({ rootId, types: 'null,supabase-image' }); + const params = new URLSearchParams({ rootId }); if (userId) params.append('userId', userId); if (mediaItem.user_id) params.append('ownerId', mediaItem.user_id); return apiClient(`/api/pictures/versions?${params}`); }); }; -export const createPicture = async (picture: Partial, client?: SupabaseClient) => { +export const createPicture = async (picture: Partial) => { return apiClient('/api/pictures', { method: 'POST', body: JSON.stringify(picture) }); }; -export const updatePicture = async (id: string, updates: Partial, client?: SupabaseClient) => { +export const updatePicture = async (id: string, updates: Partial) => { return apiClient(`/api/pictures/${id}`, { method: 'PATCH', body: JSON.stringify(updates) }); }; -export const deletePicture = async (id: string, client?: SupabaseClient) => { +export const deletePicture = async (id: string) => { return apiClient(`/api/pictures/${id}`, { method: 'DELETE' }); @@ -79,7 +78,7 @@ export const fetchUserPictures = async (userId: string) => (await fetchPictures( /** Convenience alias: fetch N recent pictures */ export const fetchRecentPictures = async (limit: number = 50) => (await fetchPictures({ limit })).data; -export const uploadFileToStorage = async (userId: string, file: File | Blob, fileName?: string, client?: SupabaseClient) => { +export const uploadFileToStorage = async (userId: string, file: File | Blob, fileName?: string) => { const uploadFile = file instanceof File ? file : new File([file], fileName || `${userId}/${Date.now()}-${Math.random().toString(36).substring(7)}`, { @@ -89,25 +88,12 @@ export const uploadFileToStorage = async (userId: string, file: File | Blob, fil return publicUrl; }; -export const toggleLike = async (userId: string, pictureId: string, isLiked: boolean, client?: SupabaseClient) => { - const { supabase } = await import("@/integrations/supabase/client"); - const db = client || supabase; - - if (isLiked) { - const { error } = await db - .from('likes') - .delete() - .eq('user_id', userId) - .eq('picture_id', pictureId); - if (error) throw error; - return false; - } else { - const { error } = await db - .from('likes') - .insert([{ user_id: userId, picture_id: pictureId }]); - if (error) throw error; - return true; - } +export const toggleLike = async (userId: string, pictureId: string, isLiked: boolean) => { + const result = await apiClient<{ liked: boolean }>(`/api/pictures/${pictureId}/like`, { + method: 'POST', + body: JSON.stringify({ userId, isLiked }), + }); + return result.liked; }; /** @@ -179,22 +165,9 @@ export async function fetchUserMediaLikes(userId: string): Promise<{ videoLikes: Set; }> { try { - const { supabase } = await import("@/integrations/supabase/client"); - // Fetch all likes (both pictures and videos are in the same table) - const { data: likesData, error: likesError } = await supabase - .from('likes') - .select('picture_id') - .eq('user_id', userId); - - if (likesError) throw likesError; - - // Since videos are also in the pictures table, all likes are in picture_id - const allLikedIds = new Set(likesData?.map(like => like.picture_id) || []); - - return { - pictureLikes: allLikedIds, - videoLikes: allLikedIds, // Same set since videos are also in pictures table - }; + const data = await apiClient<{ ids: string[] }>(`/api/me/likes?userId=${userId}`); + const allLikedIds = new Set(data.ids || []); + return { pictureLikes: allLikedIds, videoLikes: allLikedIds }; } catch (error) { console.error('Error fetching user likes:', error); throw error; @@ -202,7 +175,7 @@ export async function fetchUserMediaLikes(userId: string): Promise<{ } -export const fetchPictureById = async (id: string, client?: SupabaseClient) => { +export const fetchPictureById = async (id: string) => { return fetchWithDeduplication(`picture-${id}`, async () => { try { return await apiClient(`/api/pictures/${id}`); @@ -221,7 +194,6 @@ export const fetchMediaItemsByIds = async ( ids: string[], options?: { maintainOrder?: boolean; - client?: SupabaseClient; } ): Promise => { const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; @@ -262,36 +234,40 @@ async function filterPrivateCollectionPictures(pictures: any[]): Promise return pictures; } -export const unlinkPictures = async (ids: string[], client?: SupabaseClient) => { +export const unlinkPictures = async (ids: string[]) => { return apiClient('/api/pictures/unlink', { method: 'POST', body: JSON.stringify({ ids }) }); }; -export const upsertPictures = async (pictures: Partial[], client?: SupabaseClient) => { +export const upsertPictures = async (pictures: Partial[]) => { return apiClient('/api/pictures/upsert', { method: 'POST', body: JSON.stringify({ pictures }) }); }; -export const addCollectionPictures = async (inserts: { collection_id: string, picture_id: string }[], client?: SupabaseClient) => { - const { supabase } = await import("@/integrations/supabase/client"); - const db = client || supabase; - const { error } = await db - .from('collection_pictures') - .insert(inserts); - if (error) throw error; +export const addCollectionPictures = async (inserts: { collection_id: string, picture_id: string }[]) => { + await apiClient('/api/collection-pictures', { + method: 'POST', + body: JSON.stringify({ inserts }), + }); }; -export const updateStorageFile = async (path: string, blob: Blob, client?: SupabaseClient) => { - const { supabase } = await import("@/integrations/supabase/client"); - const db = client || supabase; - const { error } = await db.storage.from('pictures').update(path, blob, { - cacheControl: '0', - upsert: true +export const updateStorageFile = async (path: string, blob: Blob) => { + const authHeaders = await getAuthHeaders(); + const headers = { ...authHeaders }; + delete (headers as any)['Content-Type']; + const formData = new FormData(); + formData.append('file', blob); + formData.append('path', path); + const { serverUrl } = await import('@/lib/db'); + const response = await fetch(`${serverUrl}/api/images/update-storage`, { + method: 'PUT', + headers, + body: formData, }); - if (error) throw error; + if (!response.ok) throw new Error(`Failed to update storage file: ${response.statusText}`); }; /** @@ -322,7 +298,7 @@ export const transformImage = async (file: File | Blob, operations: any[]): Prom return URL.createObjectURL(resultBlob); }; -export const fetchSelectedVersions = async (rootIds: string[], client?: SupabaseClient) => { +export const fetchSelectedVersions = async (rootIds: string[]) => { if (rootIds.length === 0) return []; const sortedIds = [...rootIds].sort(); diff --git a/packages/ui/src/modules/posts/client-posts.ts b/packages/ui/src/modules/posts/client-posts.ts index 898ec2f2..e8c5dd21 100644 --- a/packages/ui/src/modules/posts/client-posts.ts +++ b/packages/ui/src/modules/posts/client-posts.ts @@ -1,7 +1,5 @@ -import { supabase as defaultSupabase } from "@/integrations/supabase/client"; import { UserProfile } from "@/modules/posts/views/types"; import { MediaType, MediaItem } from "@/types"; -import { SupabaseClient } from "@supabase/supabase-js"; import { fetchWithDeduplication, getAuthToken } from "@/lib/db"; export interface FeedPost { @@ -65,7 +63,7 @@ export const fetchPostsList = async (options: { page?: number; limit?: number; u return await res.json() as { data: any[]; count: number; page: number; limit: number }; }; -export const fetchPostById = async (id: string, client?: SupabaseClient) => { +export const fetchPostById = async (id: string) => { // Use API-mediated fetching instead of direct Supabase calls // This returns enriched FeedPost data including category_paths, author info, etc. return fetchWithDeduplication(`post-${id}`, async () => { @@ -76,21 +74,11 @@ export const fetchPostById = async (id: string, client?: SupabaseClient) => { }; -export const fetchFullPost = async (postId: string, client?: SupabaseClient) => { - const supabase = client || defaultSupabase; - return fetchWithDeduplication(`full-post-${postId}`, async () => { - const { data, error } = await supabase - .from('posts') - .select(`*, pictures (*)`) - .eq('id', postId) - .single(); - - if (error) throw error; - return data; - }); +export const fetchFullPost = async (postId: string) => { + return fetchWithDeduplication(`full-post-${postId}`, () => fetchPostDetailsAPI(postId)); }; -export const deletePost = async (id: string, client?: SupabaseClient) => { +export const deletePost = async (id: string) => { const token = await getAuthToken(); if (!token) throw new Error('No active session'); @@ -127,7 +115,7 @@ export const createPost = async (postData: { title: string, description?: string return await response.json(); }; -export const updatePostDetails = async (postId: string, updates: { title?: string, description?: string, settings?: any, meta?: any }, client?: SupabaseClient) => { +export const updatePostDetails = async (postId: string, updates: { title?: string, description?: string, settings?: any, meta?: any }) => { const token = await getAuthToken(); if (!token) throw new Error('No active session'); @@ -244,7 +232,7 @@ export const augmentFeedPosts = (posts: any[]): FeedPost[] => { }); }; -export const updatePostMeta = async (postId: string, meta: any, client?: SupabaseClient) => { +export const updatePostMeta = async (postId: string, meta: any) => { const token = await getAuthToken(); if (!token) throw new Error('No active session'); diff --git a/packages/ui/src/modules/posts/views/renderers/ArticleRenderer.tsx b/packages/ui/src/modules/posts/views/renderers/ArticleRenderer.tsx index e80d193b..466d2375 100644 --- a/packages/ui/src/modules/posts/views/renderers/ArticleRenderer.tsx +++ b/packages/ui/src/modules/posts/views/renderers/ArticleRenderer.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { supabase } from "@/integrations/supabase/client"; +import { apiClient } from "@/lib/db"; import { Link, useNavigate } from "react-router-dom"; import { User as UserIcon, LayoutGrid, StretchHorizontal, FileText, Save, X, Edit3, MoreVertical, Trash2, ArrowUp, ArrowDown, Heart, MessageCircle, Maximize, ImageIcon, Youtube, Music, Wand2, Map, Brush, Mail, Archive } from 'lucide-react'; import { useOrganization } from "@/contexts/OrganizationContext"; @@ -30,11 +30,12 @@ const CommentCountButton = ({ pictureId, isOpen, onClick }: { pictureId: string, useEffect(() => { const fetchCount = async () => { - const { count } = await supabase - .from('comments') - .select('*', { count: 'exact', head: true }) - .eq('picture_id', pictureId); - setCount(count); + try { + const data = await apiClient<{ comments: any[] }>(`/api/pictures/${pictureId}/comments`); + setCount(data.comments?.length ?? null); + } catch { + setCount(null); + } }; fetchCount(); }, [pictureId]); diff --git a/packages/ui/src/modules/user/client-user.ts b/packages/ui/src/modules/user/client-user.ts index 6985b4f0..c903048b 100644 --- a/packages/ui/src/modules/user/client-user.ts +++ b/packages/ui/src/modules/user/client-user.ts @@ -1,7 +1,5 @@ -import { supabase as defaultSupabase } from "@/integrations/supabase/client"; import { UserProfile } from "@/modules/posts/views/types"; -import { SupabaseClient } from "@supabase/supabase-js"; -import { fetchWithDeduplication, getAuthToken as getZitadelToken } from "@/lib/db"; +import { fetchWithDeduplication, apiClient, getAuthToken as getZitadelToken } from "@/lib/db"; const serverUrl = (path: string) => { const baseUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin; @@ -18,22 +16,14 @@ export const fetchProfileAPI = async (userId: string): Promise<{ profile: any; r return await res.json(); }; -export const fetchAuthorProfile = async (userId: string, client?: SupabaseClient): Promise => { - const supabase = client || defaultSupabase; +export const fetchAuthorProfile = async (userId: string): Promise => { 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; + const result = await fetchProfileAPI(userId); + return (result?.profile as UserProfile) ?? null; }); }; -export const getUserSettings = async (userId: string, client?: SupabaseClient) => { +export const getUserSettings = async (userId: string) => { return fetchWithDeduplication(`settings-${userId}`, async () => { const token = await getAuthToken(); const res = await fetch(serverUrl('/api/me/settings'), { @@ -44,7 +34,7 @@ export const getUserSettings = async (userId: string, client?: SupabaseClient) = }, 100000); }; -export const updateUserSettings = async (userId: string, settings: any, client?: SupabaseClient) => { +export const updateUserSettings = async (userId: string, settings: any) => { const token = await getAuthToken(); const res = await fetch(serverUrl('/api/me/settings'), { method: 'PATCH', @@ -57,7 +47,7 @@ export const updateUserSettings = async (userId: string, settings: any, client?: if (!res.ok) throw new Error(`Failed to update settings: ${res.statusText}`); }; -export const getUserOpenAIKey = async (userId: string, client?: SupabaseClient) => { +export const getUserOpenAIKey = async (userId: string) => { return fetchWithDeduplication(`openai-${userId}`, async () => { const token = await getAuthToken(); const res = await fetch(serverUrl('/api/me/secrets'), { @@ -72,7 +62,7 @@ export const getUserOpenAIKey = async (userId: string, client?: SupabaseClient) } /** Get all API keys (masked) from server proxy */ -export const getUserApiKeys = async (userId: string, client?: SupabaseClient): Promise | null> => { +export const getUserApiKeys = async (userId: string): Promise | null> => { return fetchWithDeduplication(`api-keys-${userId}`, async () => { const token = await getAuthToken(); const res = await fetch(serverUrl('/api/me/secrets'), { @@ -84,7 +74,7 @@ export const getUserApiKeys = async (userId: string, client?: SupabaseClient): P }); }; -export const getUserGoogleApiKey = async (userId: string, client?: SupabaseClient) => { +export const getUserGoogleApiKey = async (userId: string) => { return fetchWithDeduplication(`google-${userId}`, async () => { const token = await getAuthToken(); const res = await fetch(serverUrl('/api/me/secrets'), { @@ -97,7 +87,7 @@ export const getUserGoogleApiKey = async (userId: string, client?: SupabaseClien }); } -export const getUserSecrets = async (userId: string, client?: SupabaseClient) => { +export const getUserSecrets = async (userId: string) => { return fetchWithDeduplication(`user-secrets-${userId}`, async () => { const token = await getAuthToken(); const res = await fetch(serverUrl('/api/me/secrets'), { @@ -109,7 +99,7 @@ export const getUserSecrets = async (userId: string, client?: SupabaseClient) => }); }; -export const getProviderConfig = async (userId: string, provider: string, client?: SupabaseClient) => { +export const getProviderConfig = async (userId: string, provider: string) => { return fetchWithDeduplication(`provider-${userId}-${provider}`, async () => { const token = await getAuthToken(); const res = await fetch(serverUrl(`/api/me/provider-config/${encodeURIComponent(provider)}`), { @@ -123,7 +113,7 @@ export const getProviderConfig = async (userId: string, provider: string, client -export const fetchUserRoles = async (userId: string, client?: SupabaseClient) => { +export const fetchUserRoles = async (userId: string) => { return fetchWithDeduplication(`roles-${userId}`, async () => { const token = await getAuthToken(); const res = await fetch(serverUrl('/api/me/roles'), { @@ -137,6 +127,19 @@ export const fetchUserRoles = async (userId: string, client?: SupabaseClient) => }); }; +/** + * Fetch the resolved app identity in one call: + * - `id` — app UUID (profiles.user_id) — use this everywhere in the app + * - `sub` — original Zitadel numeric sub from the OIDC token + * - `roles` — the user's roles array + * + * No cache key here since it's called once per auth session and + * the result drives the AuthContext state. + */ +export const fetchUserIdentity = async (): Promise<{ id: string; sub: string; roles: string[] }> => { + return apiClient<{ id: string; sub: string; roles: string[] }>('/api/me/identity'); +}; + /** * Update user secrets via server proxy (API keys are merged, other fields replaced). */ @@ -219,7 +222,7 @@ export interface SavedShippingAddress { } /** Get shipping addresses via server proxy */ -export const getShippingAddresses = async (userId: string, client?: SupabaseClient): Promise => { +export const getShippingAddresses = async (userId: string): Promise => { try { const token = await getAuthToken(); const res = await fetch(serverUrl('/api/me/secrets'), { @@ -235,7 +238,7 @@ export const getShippingAddresses = async (userId: string, client?: SupabaseClie }; /** Save shipping addresses via server proxy (full replace) */ -export const saveShippingAddresses = async (userId: string, addresses: SavedShippingAddress[], client?: SupabaseClient): Promise => { +export const saveShippingAddresses = async (userId: string, addresses: SavedShippingAddress[]): Promise => { try { const token = await getAuthToken(); const res = await fetch(serverUrl('/api/me/secrets'), { @@ -277,7 +280,7 @@ export interface VendorProfile { } /** Get vendor profiles via server proxy */ -export const getVendorProfiles = async (userId: string, client?: SupabaseClient): Promise => { +export const getVendorProfiles = async (userId: string): Promise => { try { const token = await getAuthToken(); const res = await fetch(serverUrl('/api/me/secrets'), { @@ -293,7 +296,7 @@ export const getVendorProfiles = async (userId: string, client?: SupabaseClient) }; /** Save vendor profiles via server proxy (full replace) */ -export const saveVendorProfiles = async (userId: string, profiles: VendorProfile[], client?: SupabaseClient): Promise => { +export const saveVendorProfiles = async (userId: string, profiles: VendorProfile[]): Promise => { try { const token = await getAuthToken(); const res = await fetch(serverUrl('/api/me/secrets'), { @@ -334,10 +337,14 @@ export const updateProfileAPI = async (profileData: { return await res.json(); }; -/** Update the current user's email (auth-level operation) */ +/** Update the current user's email via server API */ export const updateUserEmail = async (newEmail: string): Promise => { - const { error } = await defaultSupabase.auth.updateUser({ email: newEmail }); - if (error) throw error; + const token = await getAuthToken(); + if (!token) throw new Error('Not authenticated'); + await apiClient('/api/me/email', { + method: 'PATCH', + body: JSON.stringify({ email: newEmail }), + }); }; // ============================================= diff --git a/packages/ui/src/pages/Auth.tsx b/packages/ui/src/pages/Auth.tsx index af772e6c..48acd482 100644 --- a/packages/ui/src/pages/Auth.tsx +++ b/packages/ui/src/pages/Auth.tsx @@ -5,7 +5,6 @@ import { Input } from '@/components/ui/input'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { useAuth } from '@/hooks/useAuth'; import { useOrganization } from '@/contexts/OrganizationContext'; -import { supabase } from '@/integrations/supabase/client'; import { useToast } from '@/hooks/use-toast'; import { Github, Mail } from 'lucide-react'; import { T, translate } from '@/i18n'; diff --git a/packages/ui/src/pages/AuthZ.tsx b/packages/ui/src/pages/AuthZ.tsx index 9cb98f04..061e764e 100644 --- a/packages/ui/src/pages/AuthZ.tsx +++ b/packages/ui/src/pages/AuthZ.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Shield, LogOut } from 'lucide-react'; +import { LogIn, LogOut, Sparkles } from 'lucide-react'; import { T, translate } from '@/i18n'; import { useAuth } from 'react-oidc-context'; import { useNavigate } from 'react-router-dom'; @@ -12,26 +12,27 @@ const AuthZ = () => { const [isLoading, setIsLoading] = useState(false); const [signingOut, setSigningOut] = useState(false); - // Monitor auth state changes and log them for debugging useEffect(() => { - console.log("🛡️ [AuthZ] Auth State Update:", { - isAuthenticated: auth.isAuthenticated, - isLoading: auth.isLoading, - hasError: !!auth.error, - errorMsg: auth.error?.message - }); - - if (auth.user) { - // `profile.email` is the same claim the API reads from the Bearer JWT (`getUserCached` → `user.email`). - console.log("🛡️ [AuthZ] Identity Token Profile:", auth.user.profile); - console.log("🛡️ [AuthZ] Access Token:", auth.user.access_token); + if (import.meta.env.DEV) { + console.log("🛡️ [AuthZ] Auth State Update:", { + isAuthenticated: auth.isAuthenticated, + isLoading: auth.isLoading, + hasError: !!auth.error, + errorMsg: auth.error?.message + }); + if (auth.user) { + console.log("🛡️ [AuthZ] Identity Token Profile:", auth.user.profile); + console.log("🛡️ [AuthZ] Access Token:", auth.user.access_token); + } } }, [auth.isAuthenticated, auth.isLoading, auth.error, auth.user]); - const handleZitadelLogin = async () => { + const handleLogin = async () => { try { setIsLoading(true); - await auth.signinRedirect(); + const params = new URLSearchParams(window.location.search); + const redirectTo = params.get('redirectTo') || '/'; + await auth.signinRedirect({ state: { redirectTo } }); } catch (e) { console.error(e); setIsLoading(false); @@ -41,37 +42,43 @@ const AuthZ = () => { const handleLogout = async () => { try { setSigningOut(true); - await auth.signoutRedirect(); + await auth.signoutRedirect({ post_logout_redirect_uri: window.location.origin + '/' }); } catch (e) { console.error(e); setSigningOut(false); } }; + // ── Signed in ───────────────────────────────────────────────────────────── if (auth.isAuthenticated && auth.user && !auth.isLoading) { - const email = auth.user.profile.email ?? auth.user.profile.preferred_username ?? ''; + const name = auth.user.profile.given_name + ?? auth.user.profile.name + ?? auth.user.profile.preferred_username + ?? auth.user.profile.email + ?? ''; + return (
- +
- Signed in + {name ? `${translate('Welcome back')}, ${name}!` : Welcome back!} - - {email || Session active} + + You're all set. Let's get to work.
- +

+ Secure sign-in via Google or company credentials +

diff --git a/packages/ui/src/pages/Collections.tsx b/packages/ui/src/pages/Collections.tsx index e521c3fa..66fd7361 100644 --- a/packages/ui/src/pages/Collections.tsx +++ b/packages/ui/src/pages/Collections.tsx @@ -1,6 +1,9 @@ import React, { useState, useEffect } from 'react'; import { useParams, Link, useNavigate } from 'react-router-dom'; -import { supabase } from '@/integrations/supabase/client'; +import { apiClient } from '@/lib/db'; +import { uploadImage } from '@/lib/uploadUtils'; +import { createPicture } from '@/modules/posts/client-pictures'; +import { fetchCommentsAPI } from '@/modules/posts/client-pictures'; import { useAuth } from '@/hooks/useAuth'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -129,44 +132,24 @@ const CollectionsContent = () => { setUploadingFiles(prev => prev.map(f => f.id === fileToUpload.id ? { ...f, status: 'uploading', progress: 10 } : f)); - const filePath = `${user.id}/${collection.id}/${Date.now()}_${fileToUpload.file.name.replace(/[^a-zA-Z0-9.\-_]/g, '')}`; - const { error: uploadError } = await supabase.storage - .from('pictures') - .upload(filePath, fileToUpload.file, { - cacheControl: '3600', - upsert: false, - }); + setUploadingFiles(prev => prev.map(f => f.id === fileToUpload.id ? { ...f, progress: 30, status: 'uploading' } : f)); - if (uploadError) throw uploadError; + const { publicUrl } = await uploadImage(fileToUpload.file, user.id); - setUploadingFiles(prev => prev.map(f => f.id === fileToUpload.id ? { ...f, progress: 90, status: 'processing' } : f)); + setUploadingFiles(prev => prev.map(f => f.id === fileToUpload.id ? { ...f, progress: 70, status: 'processing' } : f)); - const { data: { publicUrl } } = supabase.storage.from('pictures').getPublicUrl(filePath); + const newPicture = await createPicture({ + user_id: user.id, + image_url: publicUrl, + thumbnail_url: publicUrl, + title: '', + description: '', + } as any) as any; - if (!publicUrl) throw new Error('Could not get public URL'); - - const { data: newPicture, error: insertPictureError } = await supabase - .from('pictures') - .insert({ - user_id: user.id, - image_url: publicUrl, - thumbnail_url: publicUrl, - title: '', - description: '', - }) - .select() - .single(); - - if (insertPictureError) throw insertPictureError; - - const { error: insertCollectionPictureError } = await supabase - .from('collection_pictures') - .insert({ - collection_id: collection.id, - picture_id: newPicture.id, - }); - - if (insertCollectionPictureError) throw insertCollectionPictureError; + await apiClient('/api/collection-pictures', { + method: 'POST', + body: JSON.stringify({ inserts: [{ collection_id: collection.id, picture_id: newPicture.id }] }), + }); setUploadingFiles(prev => prev.map(f => f.id === fileToUpload.id ? { ...f, status: 'complete', progress: 100 } : f)); @@ -206,56 +189,27 @@ const CollectionsContent = () => { try { setLoading(true); - // Fetch collection - const { data: collectionData, error: collectionError } = await supabase - .from('collections') - .select('*') - .eq('user_id', userId) - .eq('slug', slug) - .single(); + const collections = await apiClient(`/api/collections?userId=${userId}&slug=${slug}`); + const collectionData = collections?.[0]; - if (collectionError) { - console.error('Error fetching collection:', collectionError); + if (!collectionData) { + console.error('Collection not found'); return; } setCollection(collectionData); - // Fetch pictures in collection - const { data: picturesData, error: picturesError } = await supabase - .from('collection_pictures') - .select(` - pictures:picture_id ( - id, - title, - description, - image_url, - thumbnail_url, - user_id, - created_at, - likes_count - ) - `) - .eq('collection_id', collectionData.id); + const picturesData = await apiClient(`/api/collection-pictures/pictures?collectionId=${collectionData.id}`); + const flattenedPictures = picturesData || []; - if (picturesError) { - console.error('Error fetching pictures:', picturesError); - return; - } - - const flattenedPictures = picturesData - .map(item => item.pictures) - .filter(Boolean) as Picture[]; - - // Add comment counts for each picture const picturesWithCommentCounts = await Promise.all( flattenedPictures.map(async (picture) => { - const { count } = await supabase - .from('comments') - .select('*', { count: 'exact', head: true }) - .eq('picture_id', picture.id); - - return { ...picture, comments: [{ count: count || 0 }] }; + try { + const result = await fetchCommentsAPI(picture.id); + return { ...picture, comments: [{ count: result.comments?.length || 0 }] }; + } catch { + return { ...picture, comments: [{ count: 0 }] }; + } }) ); @@ -288,38 +242,18 @@ const CollectionsContent = () => { if (!collection || !user) return; try { - // Find the picture that was just published by URL - const { data: picture, error: findError } = await supabase - .from('pictures') - .select('id') - .eq('image_url', imageUrl) - .eq('user_id', user.id) - .order('created_at', { ascending: false }) - .limit(1) - .single(); + const pics = await apiClient(`/api/pictures?userId=${user.id}&imageUrl=${encodeURIComponent(imageUrl)}&limit=1`); + const picture = pics?.[0]; - if (findError || !picture) { - console.error('Error finding published picture:', findError); + if (!picture) { + console.error('Could not find published picture'); return; } - // Add the picture to this collection - const { error: addError } = await supabase - .from('collection_pictures') - .insert({ - collection_id: collection.id, - picture_id: picture.id - }); - - if (addError) { - console.error('Error adding to collection:', addError); - toast({ - title: "Error", - description: "Failed to add image to collection", - variant: "destructive" - }); - return; - } + await apiClient('/api/collection-pictures', { + method: 'POST', + body: JSON.stringify({ inserts: [{ collection_id: collection.id, picture_id: picture.id }] }), + }); toast({ title: "Success!", @@ -355,18 +289,15 @@ const CollectionsContent = () => { .replace(/[\s_-]+/g, '-') .replace(/^-+|-+$/g, ''); - const { error } = await supabase - .from('collections') - .update({ + await apiClient(`/api/collections/${collection.id}`, { + method: 'PATCH', + body: JSON.stringify({ name: editForm.name.trim(), description: editForm.description.trim() || null, is_public: editForm.is_public, - slug: newSlug - }) - .eq('id', collection.id) - .eq('user_id', user.id); - - if (error) throw error; + slug: newSlug, + }), + }); toast({ title: "Success!", @@ -398,22 +329,11 @@ const CollectionsContent = () => { setIsDeleting(true); try { - // First delete all collection_pictures entries - const { error: deletePicturesError } = await supabase - .from('collection_pictures') - .delete() - .eq('collection_id', collection.id); - - if (deletePicturesError) throw deletePicturesError; - - // Then delete the collection itself - const { error: deleteError } = await supabase - .from('collections') - .delete() - .eq('id', collection.id) - .eq('user_id', user.id); - - if (deleteError) throw deleteError; + await apiClient('/api/collection-pictures', { + method: 'DELETE', + body: JSON.stringify({ collection_id: collection.id, deleteAll: true }), + }); + await apiClient(`/api/collections/${collection.id}`, { method: 'DELETE' }); toast({ title: "Success!", @@ -568,7 +488,7 @@ const CollectionsContent = () => { {/* Images Grid */} ({ ...p, type: 'supabase-image', meta: {} }))} + customPictures={pictures.map(p => ({ ...p, type: 'image', meta: {} }))} customLoading={loading} navigationSource="collection" navigationSourceId={collection?.id} diff --git a/packages/ui/src/pages/NewCollection.tsx b/packages/ui/src/pages/NewCollection.tsx index ff41addb..ba11e14f 100644 --- a/packages/ui/src/pages/NewCollection.tsx +++ b/packages/ui/src/pages/NewCollection.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { supabase } from "@/integrations/supabase/client"; +import { apiClient } from "@/lib/db"; import { useAuth } from "@/hooks/useAuth"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; @@ -46,38 +46,20 @@ const NewCollection = () => { try { const slug = generateSlug(name); - - // Check if slug already exists for this user - const { data: existing } = await supabase - .from('collections') - .select('id') - .eq('user_id', user.id) - .eq('slug', slug) - .single(); - if (existing) { - toast.error("You already have a collection with this name"); - setIsCreating(false); - return; - } - - // Create the collection - const { data, error } = await supabase - .from('collections') - .insert({ + const data = await apiClient('/api/collections', { + method: 'POST', + body: JSON.stringify({ user_id: user.id, name: name.trim(), description: description.trim() || null, - slug: slug, - is_public: isPublic - }) - .select() - .single(); - - if (error) throw error; + slug, + is_public: isPublic, + }), + }); toast.success("Collection created successfully!"); - navigate(`/collections/${user.id}/${slug}`); + navigate(`/collections/${user.id}/${data.slug || slug}`); } catch (error) { console.error('Error creating collection:', error); toast.error("Failed to create collection"); diff --git a/packages/ui/src/pages/Organizations.tsx b/packages/ui/src/pages/Organizations.tsx index 8d68194e..8decab3d 100644 --- a/packages/ui/src/pages/Organizations.tsx +++ b/packages/ui/src/pages/Organizations.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { supabase } from "@/integrations/supabase/client"; +import { apiClient } from "@/lib/db"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -30,29 +30,13 @@ export default function Organizations() { // Fetch organizations const { data: organizations, isLoading } = useQuery({ queryKey: ["organizations"], - queryFn: async () => { - const { data, error } = await supabase - .from("organizations") - .select("*") - .order("created_at", { ascending: false }); - - if (error) throw error; - return data as Organization[]; - }, + queryFn: () => apiClient('/api/organizations'), }); // Create organization mutation const createMutation = useMutation({ - mutationFn: async (org: { name: string; slug: string }) => { - const { data, error } = await supabase - .from("organizations") - .insert([org]) - .select() - .single(); - - if (error) throw error; - return data; - }, + mutationFn: (org: { name: string; slug: string }) => + apiClient('/api/organizations', { method: 'POST', body: JSON.stringify(org) }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["organizations"] }); toast.success("Organization created successfully"); @@ -67,17 +51,8 @@ export default function Organizations() { // Update organization mutation const updateMutation = useMutation({ - mutationFn: async (org: { id: string; name: string; slug: string }) => { - const { data, error } = await supabase - .from("organizations") - .update({ name: org.name, slug: org.slug }) - .eq("id", org.id) - .select() - .single(); - - if (error) throw error; - return data; - }, + mutationFn: (org: { id: string; name: string; slug: string }) => + apiClient(`/api/organizations/${org.id}`, { method: 'PATCH', body: JSON.stringify({ name: org.name, slug: org.slug }) }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["organizations"] }); toast.success("Organization updated successfully"); @@ -91,14 +66,7 @@ export default function Organizations() { // Delete organization mutation const deleteMutation = useMutation({ - mutationFn: async (id: string) => { - const { error } = await supabase - .from("organizations") - .delete() - .eq("id", id); - - if (error) throw error; - }, + mutationFn: (id: string) => apiClient(`/api/organizations/${id}`, { method: 'DELETE' }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["organizations"] }); toast.success("Organization deleted successfully"); diff --git a/packages/ui/src/pages/PlaygroundImageEditor.tsx b/packages/ui/src/pages/PlaygroundImageEditor.tsx index a16cce17..1e2c9d93 100644 --- a/packages/ui/src/pages/PlaygroundImageEditor.tsx +++ b/packages/ui/src/pages/PlaygroundImageEditor.tsx @@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; import { ImagePickerDialog } from '@/components/widgets/ImagePickerDialog'; import { RotateCw, RotateCcw, Crop as CropIcon, Download, Sliders, Image as ImageIcon, X, Check, Save } from 'lucide-react'; -import { supabase } from '@/integrations/supabase/client'; +import { fetchPictureById, updatePicture } from '@/modules/posts/client-pictures'; import { useAuth } from '@/hooks/useAuth'; import { useToast } from '@/components/ui/use-toast'; import { cn } from '@/lib/utils'; @@ -46,13 +46,8 @@ const PlaygroundImageEditor = () => { setLoading(true); setPickerOpen(false); try { - const { data, error } = await supabase - .from('pictures') - .select('*') - .eq('id', pictureId) - .single(); - - if (error) throw error; + const data = await fetchPictureById(pictureId); + if (!data) throw new Error('Picture not found'); setSelectedPictureId(pictureId); setSourceImage(data.image_url); @@ -133,15 +128,7 @@ const PlaygroundImageEditor = () => { const { publicUrl } = await uploadImage(file, user.id); // 3. Update Database Record - const { error: dbError } = await supabase - .from('pictures') - .update({ - image_url: publicUrl, - updated_at: new Date().toISOString() - }) - .eq('id', selectedPictureId); - - if (dbError) throw dbError; + await updatePicture(selectedPictureId, { image_url: publicUrl } as any); setSourceImage(publicUrl); toast({ title: "Image Saved", description: "Source image updated successfully." }); diff --git a/packages/ui/src/pages/TagPage.tsx b/packages/ui/src/pages/TagPage.tsx index 3a91fd37..cb8d9600 100644 --- a/packages/ui/src/pages/TagPage.tsx +++ b/packages/ui/src/pages/TagPage.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from "react"; import { useParams, useNavigate, Link } from "react-router-dom"; -import { supabase } from "@/integrations/supabase/client"; +import { apiClient } from "@/lib/db"; +import { fetchPictures, fetchCommentsAPI, toggleLike } from "@/modules/posts/client-pictures"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -60,54 +61,33 @@ const TagPage = () => { try { setLoading(true); - // Fetch pictures with tag in tags array or description - const { data: picturesData, error: picturesError } = await supabase - .from('pictures') - .select('*') - .or(`tags.cs.{${normalizedTag}},tags.cs.{"${normalizedTag}"},description.ilike.%#${normalizedTag}%,title.ilike.%#${normalizedTag}%`) - .order('created_at', { ascending: false }); + const result = await fetchPictures({ tag: normalizedTag }); + const picturesData = result.data || []; + const publicPictures = await filterPrivateCollectionPictures(picturesData); - if (picturesError) throw picturesError; - - // Filter out pictures that are only in private collections - const publicPictures = await filterPrivateCollectionPictures(picturesData || []); - - // Get comment counts for each picture const picturesWithCommentCounts = await Promise.all( publicPictures.map(async (picture) => { - const { count } = await supabase - .from('comments') - .select('*', { count: 'exact', head: true }) - .eq('picture_id', picture.id); - - return { ...picture, comments: [{ count: count || 0 }] }; + try { + const commentsResult = await fetchCommentsAPI(picture.id); + return { ...picture, comments: [{ count: commentsResult.comments?.length || 0 }] }; + } catch { + return { ...picture, comments: [{ count: 0 }] }; + } }) ); setPictures(picturesWithCommentCounts); - // Fetch comments containing the hashtag with their associated pictures - const { data: commentsData, error: commentsError } = await supabase - .from('comments') - .select('*') - .ilike('content', `%#${normalizedTag}%`) - .order('created_at', { ascending: false }); + const commentsData = await apiClient(`/api/comments?tag=${encodeURIComponent('#' + normalizedTag)}`).catch(() => []); - if (commentsError) throw commentsError; - - // Get picture details for each comment const commentsWithPictures = await Promise.all( (commentsData || []).map(async (comment) => { - const { data: picture } = await supabase - .from('pictures') - .select('title, image_url') - .eq('id', comment.picture_id) - .single(); - - return { - ...comment, - picture: picture || { title: 'Unknown', image_url: '' } - }; + try { + const picture = await apiClient(`/api/pictures/${comment.picture_id}`); + return { ...comment, picture: { title: picture?.title || 'Unknown', image_url: picture?.image_url || '' } }; + } catch { + return { ...comment, picture: { title: 'Unknown', image_url: '' } }; + } }) ); @@ -125,13 +105,8 @@ const TagPage = () => { if (!user) return; try { - const { data, error } = await supabase - .from('likes') - .select('picture_id') - .eq('user_id', user.id); - - if (error) throw error; - setUserLikes(new Set(data.map(like => like.picture_id))); + const data = await apiClient<{ ids: string[] }>(`/api/me/likes?userId=${user.id}`); + setUserLikes(new Set(data.ids || [])); } catch (error) { console.error('Error fetching user likes:', error); } diff --git a/packages/ui/src/pages/UpdatePassword.tsx b/packages/ui/src/pages/UpdatePassword.tsx index caa7bcc6..512fb52f 100644 --- a/packages/ui/src/pages/UpdatePassword.tsx +++ b/packages/ui/src/pages/UpdatePassword.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { supabase } from '@/integrations/supabase/client'; +import { apiClient } from '@/lib/db'; import { useToast } from '@/hooks/use-toast'; const UpdatePassword = () => { @@ -35,21 +35,14 @@ const UpdatePassword = () => { } setLoading(true); - const { error } = await supabase.auth.updateUser({ password }); - setLoading(false); - - if (error) { - toast({ - variant: 'destructive', - title: 'Update failed', - description: error.message, - }); - } else { - toast({ - title: 'Password updated', - description: 'Your password has been changed successfully.', - }); + try { + await apiClient('/api/me/password', { method: 'PATCH', body: JSON.stringify({ password }) }); + toast({ title: 'Password updated', description: 'Your password has been changed successfully.' }); navigate('/'); + } catch (err: any) { + toast({ variant: 'destructive', title: 'Update failed', description: err.message }); + } finally { + setLoading(false); } }; diff --git a/packages/ui/src/pages/UserCollections.tsx b/packages/ui/src/pages/UserCollections.tsx index c70580c3..093101fa 100644 --- a/packages/ui/src/pages/UserCollections.tsx +++ b/packages/ui/src/pages/UserCollections.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from "react"; import { useParams, useNavigate, Link } from "react-router-dom"; -import { supabase } from "@/integrations/supabase/client"; +import { apiClient } from "@/lib/db"; +import { fetchAuthorProfile } from "@/modules/user/client-user"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -45,16 +46,7 @@ const UserCollections = () => { const fetchUserProfile = async () => { try { - const { data: profile, error: profileError } = await supabase - .from('profiles') - .select('*') - .eq('user_id', userId) - .maybeSingle(); - - if (profileError && profileError.code !== 'PGRST116') { - throw profileError; - } - + const profile = await fetchAuthorProfile(userId!); if (!profile) { setUserProfile({ id: userId!, @@ -66,12 +58,12 @@ const UserCollections = () => { }); } else { setUserProfile({ - id: profile.user_id, - username: profile.username, - display_name: profile.display_name || `User ${userId!.slice(0, 8)}`, - bio: profile.bio, - avatar_url: profile.avatar_url, - created_at: profile.created_at, + id: (profile as any).user_id || userId!, + username: (profile as any).username || null, + display_name: (profile as any).display_name || `User ${userId!.slice(0, 8)}`, + bio: (profile as any).bio || null, + avatar_url: (profile as any).avatar_url || null, + created_at: (profile as any).created_at || new Date().toISOString(), }); } } catch (error) { @@ -83,30 +75,18 @@ const UserCollections = () => { const fetchCollections = async () => { try { - let query = supabase - .from('collections') - .select('*'); + const params = new URLSearchParams({ userId: userId! }); + if (!isOwnProfile) params.set('is_public', 'true'); + const collectionsData = await apiClient(`/api/collections?${params}`); - // If viewing own profile, show all collections; otherwise only public ones - if (isOwnProfile) { - query = query.eq('user_id', userId); - } else { - query = query.eq('user_id', userId).eq('is_public', true); - } - - const { data: collectionsData, error } = await query.order('created_at', { ascending: false }); - - if (error) throw error; - - // Get picture count for each collection const collectionsWithCounts = await Promise.all( (collectionsData || []).map(async (collection) => { - const { count } = await supabase - .from('collection_pictures') - .select('*', { count: 'exact', head: true }) - .eq('collection_id', collection.id); - - return { ...collection, picture_count: count || 0 }; + try { + const data = await apiClient<{ picture_id: string }[]>(`/api/collection-pictures?collectionId=${collection.id}`); + return { ...collection, picture_count: data?.length || 0 }; + } catch { + return { ...collection, picture_count: 0 }; + } }) ); diff --git a/packages/ui/src/pages/VideoFeedPlayground.tsx b/packages/ui/src/pages/VideoFeedPlayground.tsx deleted file mode 100644 index d2e3706d..00000000 --- a/packages/ui/src/pages/VideoFeedPlayground.tsx +++ /dev/null @@ -1,521 +0,0 @@ -/** - * Video Feed Playground - * TikTok-style infinite scroll video player using our database videos - */ - -import React, { useEffect, useState, useCallback, useRef } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { VideoFeed } from '@/player/components/VideoFeed'; -import { VideoItem } from '@/player/types'; -import { MediaItem } from '@/utils/mediaUtils'; -import { supabase } from '@/integrations/supabase/client'; -import { Button } from '@/components/ui/button'; -import { ArrowLeft } from 'lucide-react'; -import { useAuth } from '@/hooks/useAuth'; -import { toast } from "sonner"; -import { fetchMediaItems } from '@/modules/posts/client-pictures'; - -// Mock data generators for TikTok features we don't have -const SAMPLE_AVATARS = [ - 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=100&h=100&fit=crop&crop=face', - 'https://images.unsplash.com/photo-1494790108755-2616b612b47c?w=100&h=100&fit=crop&crop=face', - 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&h=100&fit=crop&crop=face', - 'https://images.unsplash.com/photo-1517841905240-472988babdf9?w=100&h=100&fit=crop&crop=face', - 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100&h=100&fit=crop&crop=face' -]; - -const SAMPLE_MUSIC = [ - { - id: 'music_1', - title: 'Original Sound', - playUrl: '', - coverMedium: SAMPLE_AVATARS[0], - authorName: 'Original Audio' - }, - { - id: 'music_2', - title: 'Trending Sound', - playUrl: '', - coverMedium: SAMPLE_AVATARS[1], - authorName: 'Popular Track' - } -]; - -/** - * Transform our MediaItem to TikTok's VideoItem format - */ -const transformMediaToVideoItem = (media: MediaItem, userProfiles: Record): VideoItem => { - const userProfile = userProfiles[media.user_id] || {}; - const isVideo = media.type === 'video'; - - // Extract hashtags from description - const hashtags = media.description?.match(/#\w+/g) || []; - - // Get display name with fallback priority: display_name > username > 'User' - const displayName = userProfile.display_name - || userProfile.username - || `User ${media.user_id.slice(0, 8)}`; - - const uniqueId = userProfile.username - || media.user_id.slice(0, 8); - - return { - id: media.id, - author: { - nickname: displayName, - uniqueId: uniqueId, - id: media.user_id, - secUid: `sec_${media.user_id}`, - // Use actual avatar or fallback to a consistent default based on user_id - avatarThumb: userProfile.avatar_url || SAMPLE_AVATARS[parseInt(media.user_id.slice(-2), 16) % SAMPLE_AVATARS.length], - verified: false - }, - video: { - width: isVideo ? (media.meta?.width || 720) : 720, - height: isVideo ? (media.meta?.height || 1280) : 1280, - duration: isVideo ? (media.meta?.duration || 30) : 30, - ratio: '9:16', - playAddr: media.url, // This will now correctly be either image_url or video_url - downloadAddr: media.url, - cover: media.thumbnail_url || media.url, - dynamicCover: media.thumbnail_url || media.url - }, - stats: { - diggCount: media.likes_count || 0, // Real likes count - playCount: Math.floor(Math.random() * 10000) + (media.likes_count * 10), // Mock, but proportional - shareCount: Math.floor(media.comments_count / 2) + Math.floor(Math.random() * 50), // Mock, but proportional - commentCount: media.comments_count || 0, // Real comments count - collectCount: Math.floor(media.likes_count / 5) + Math.floor(Math.random() * 10) // Mock, but proportional - }, - desc: media.description || media.title || 'Check out this content! ' + hashtags.slice(0, 3).join(' '), - createTime: new Date(media.created_at).getTime(), - isPinnedItem: false, - // Mock data for TikTok features we don't support yet - music: SAMPLE_MUSIC[Math.floor(Math.random() * SAMPLE_MUSIC.length)], - challenges: hashtags.slice(0, 2).map((tag, idx) => ({ - id: `challenge_${idx}`, - title: tag.replace('#', ''), - desc: `Join the ${tag} challenge` - })), - effectStickers: [] // No effects for now - }; -}; - -export default function VideoFeedPlayground() { - const navigate = useNavigate(); - const { user } = useAuth(); - const { id } = useParams<{ id?: string }>(); - const [videos, setVideos] = useState([]); - const [loading, setLoading] = useState(true); - const [hasMore, setHasMore] = useState(true); - const [currentPage, setCurrentPage] = useState(0); - const [userProfiles, setUserProfiles] = useState>({}); - const [likedVideos, setLikedVideos] = useState>(new Set()); - const [error, setError] = useState(null); - - // Use a ref to hold the latest userProfiles to avoid stale state in callbacks - const userProfilesRef = useRef(userProfiles); - userProfilesRef.current = userProfiles; - - const VIDEOS_PER_PAGE = 10; - - /** - * Fetch user profiles for video authors - * This function now only fetches and returns new profiles, and updates state separately. - */ - const fetchUserProfiles = useCallback(async (userIds: string[]) => { - try { - // Filter out user IDs that are already in our cache - const uniqueUserIds = [...new Set(userIds)].filter(id => !userProfilesRef.current[id]); - if (uniqueUserIds.length === 0) return {}; - - console.log('Fetching profiles for users:', uniqueUserIds); - const { data, error } = await supabase - .from('profiles') - .select('user_id, avatar_url, display_name, username') - .in('user_id', uniqueUserIds); - - if (error && error.code !== 'PGRST116') { - throw error; - } - - const profileMap: Record = {}; - data?.forEach(profile => { - profileMap[profile.user_id] = profile; - }); - - return profileMap; - } catch (err) { - console.error('Error fetching user profiles:', err); - return {}; - } - }, []); - - /** - * Check which videos the current user has liked - */ - const checkLikedVideos = useCallback(async (videoIds: string[]) => { - if (!user) return; - try { - const { data, error } = await supabase - .from('likes') - .select('picture_id') - .in('picture_id', videoIds) - .eq('user_id', user.id); - - if (error) throw error; - - const likedIds = new Set(data.map(like => like.picture_id)); - setLikedVideos(prev => new Set([...prev, ...likedIds])); - } catch (err) { - console.error('Error checking liked videos:', err); - } - }, [user]); - - /** - * Load initial videos - */ - const loadInitialVideos = useCallback(async () => { - try { - setLoading(true); - setError(null); - - // Fallback to fetching from DB if no navigation data - let initialMediaItems: MediaItem[] = []; - - // If an ID is present in the URL, fetch that video first - if (id) { - const { data, error } = await supabase - .from('pictures') - .select('*') - .eq('id', id) - .single(); - - if (error) { - console.error('Error fetching specific video:', error); - setError('Could not load the requested video.'); - // Continue to load the regular feed - } else if (data) { - // Manually construct a partial MediaItem to ensure the URL is correctly assigned - const isMuxVideo = data.type === 'mux-video'; - const isLegacyVideo = data.type === 'video'; - - let url = data.image_url; - if (isMuxVideo && data.meta?.mux_playback_id) { - url = `https://stream.mux.com/${data.meta.mux_playback_id}.m3u8`; - } else if (isLegacyVideo) { - url = data.video_url; - } - - initialMediaItems.push({ - ...data, - url: url, - } as MediaItem); - } - } - - // Fetch more media items to fill the page - const mediaItems = await fetchMediaItems({ - limit: VIDEOS_PER_PAGE, - includePrivate: false, - // Exclude the video we might have already fetched - excludeIds: initialMediaItems.map(item => item.id) - }); - - // Combine the specific video with the rest of the feed - const allMediaItems = [...initialMediaItems, ...mediaItems.filter(item => !initialMediaItems.find(i => i.id === item.id))]; - - // Filter to only videos - const videoItems = allMediaItems.filter(item => item.type === 'video'); - - if (videoItems.length === 0) { - setError('No videos found. Please upload some videos to test the player.'); - setLoading(false); - return; - } - - // Fetch user profiles and update state - const userIds = videoItems.map(v => v.user_id); - const newProfiles = await fetchUserProfiles(userIds); - setUserProfiles(prev => ({ ...prev, ...newProfiles })); - - // Check for liked status - await checkLikedVideos(videoItems.map(v => v.id)); - - // Use the latest profiles for transformation - const allProfiles = { ...userProfilesRef.current, ...newProfiles }; - const transformedVideos = videoItems.map(item => - transformMediaToVideoItem(item, allProfiles) - ); - - setVideos(transformedVideos); - setHasMore(videoItems.length >= VIDEOS_PER_PAGE); - setCurrentPage(1); - - // Set initial URL to the first video if not already on a specific video URL - if (transformedVideos.length > 0 && !id) { - navigate(`/video-feed/${transformedVideos[0].id}`, { replace: true }); - } - } catch (err) { - console.error('Error loading videos:', err); - setError('Failed to load videos. Please try again.'); - } finally { - setLoading(false); - } - }, [id, navigationData, fetchUserProfiles, checkLikedVideos, navigate]); - - /** - * Load more videos for infinite scroll - */ - const loadMoreVideos = useCallback(async () => { - if (loading || !hasMore) return; - - try { - setLoading(true); - - const mediaItems = await fetchMediaItems({ - limit: VIDEOS_PER_PAGE, - includePrivate: false, - offset: currentPage * VIDEOS_PER_PAGE - }); - - const videoItems = mediaItems.filter(item => item.type === 'video'); - - if (videoItems.length < VIDEOS_PER_PAGE) { - setHasMore(false); - } - - // Fetch user profiles for new videos - const userIds = videoItems.map(v => v.user_id); - const newProfiles = await fetchUserProfiles(userIds); - setUserProfiles(prev => ({ ...prev, ...newProfiles })); - - // Check for liked status - await checkLikedVideos(videoItems.map(v => v.id)); - - // Transform to TikTok format - const allProfiles = { ...userProfilesRef.current, ...newProfiles }; - const transformedVideos = videoItems.map(item => - transformMediaToVideoItem(item, allProfiles) - ); - - setVideos(prev => { - const existingIds = new Set(prev.map(v => v.id)); - const newVideos = transformedVideos.filter(v => !existingIds.has(v.id)); - return [...prev, ...newVideos]; - }); - setCurrentPage(prev => prev + 1); - } catch (err) { - console.error('Error loading more videos:', err); - } finally { - setLoading(false); - } - }, [loading, hasMore, currentPage, fetchUserProfiles, checkLikedVideos]); - - // Load initial videos on mount - useEffect(() => { - // Only fetch videos if the list is empty. This prevents re-fetching when the URL - // changes due to scrolling in the feed. The `id` from `useParams` is still respected - // by `loadInitialVideos` on the first load. - if (videos.length === 0) { - loadInitialVideos(); - } - }, [loadInitialVideos, videos.length]); - - // Clear navigation data on unmount - useEffect(() => { - return () => { - - }; - }, []); - - /** - * Handle video interactions - */ - const handleLike = useCallback(async (videoId: string) => { - if (!user) { - toast.error('Please sign in to like videos'); - return; - } - - const isLiked = likedVideos.has(videoId); - - if (isLiked) { - // UNLIKE LOGIC - try { - const { error } = await supabase - .from('likes') - .delete() - .eq('user_id', user.id) - .eq('picture_id', videoId); - - if (error) throw error; - - // Update state on success - setLikedVideos(prev => { - const newLiked = new Set(prev); - newLiked.delete(videoId); - return newLiked; - }); - setVideos(prevVideos => { - const newVideos = [...prevVideos]; - const videoIndex = newVideos.findIndex(v => v.id === videoId); - if (videoIndex !== -1) { - newVideos[videoIndex].stats.diggCount = Math.max(0, newVideos[videoIndex].stats.diggCount - 1); - } - return newVideos; - }); - - } catch (error) { - console.error('Error unliking video:', error); - toast.error('Failed to unlike video.'); - } - } else { - // LIKE LOGIC - try { - const { error } = await supabase - .from('likes') - .insert([{ user_id: user.id, picture_id: videoId }]); - - if (error) throw error; - - // Update state on success - setLikedVideos(prev => { - const newLiked = new Set(prev); - newLiked.add(videoId); - return newLiked; - }); - setVideos(prevVideos => { - const newVideos = [...prevVideos]; - const videoIndex = newVideos.findIndex(v => v.id === videoId); - if (videoIndex !== -1) { - newVideos[videoIndex].stats.diggCount++; - } - return newVideos; - }); - - } catch (error) { - if ((error as any).code === '23505') { - // This case means client state is out of sync with DB. - // The video is already liked in DB. Just update client state. - console.warn('Like already exists in DB, syncing client state.'); - setLikedVideos(prev => { - const newLiked = new Set(prev); - newLiked.add(videoId); - return newLiked; - }); - } else { - console.error('Error liking video:', error); - toast.error('Failed to like video.'); - } - } - } - }, [user, likedVideos]); - - const handleComment = (videoId: string) => { - console.log('Comment on video:', videoId); - // This is handled by the VideoFeed component now - }; - - const handleShare = (videoId: string) => { - const videoUrl = `${window.location.origin}/video/${videoId}`; - navigator.clipboard?.writeText(videoUrl).then(() => { - console.log('Video URL copied:', videoUrl); - }); - }; - - const handleFollow = (userId: string) => { - console.log('Follow user:', userId); - // TODO: Implement follow functionality - }; - - const handleAvatarClick = (userId: string) => { - if (userId) { - navigate(`/user/${userId}`); - } - }; - - const handleVideoChange = useCallback((index: number) => { - if (videos && videos[index]) { - const videoId = videos[index].id; - // Update URL without navigating away. `replace: true` avoids polluting browser history. - navigate(`/video-feed/${videoId}`, { replace: true }); - } - }, [navigate, videos]); - - if (error && videos.length === 0) { - return ( -
-
-

Video Feed Playground

-
-

{error}

-
-
- - -
-
-
- ); - } - - if (loading && videos.length === 0) { - return ( -
-
-
-

Loading videos...

-
-
- ); - } - - return ( -
- {/* Back button overlay */} -
- -
- - - {/* Video Feed */} - - -
- ); -} - diff --git a/packages/ui/src/pages/VideoGenPlayground.tsx b/packages/ui/src/pages/VideoGenPlayground.tsx index b5a1ff3b..8c93c1ff 100644 --- a/packages/ui/src/pages/VideoGenPlayground.tsx +++ b/packages/ui/src/pages/VideoGenPlayground.tsx @@ -11,7 +11,7 @@ import { Loader2, AlertCircle, CheckCircle2, Image as ImageIcon, Upload } from " // Import from updated video-router import { generateVideo, AVAILABLE_VIDEO_MODELS, VideoGenerationOptions } from "@/lib/video-router"; import { ImagePickerDialog } from "@/components/widgets/ImagePickerDialog"; -import { supabase } from "@/integrations/supabase/client"; +import { fetchPictureById } from "@/modules/posts/client-pictures"; import { VideoSettingsControls } from "@/components/video/VideoSettingsControls"; const VideoGenPlayground = () => { @@ -172,12 +172,7 @@ const VideoGenPlayground = () => { const handleImageSelect = async (pictureId: string) => { setShowImagePicker(false); - const { data, error } = await supabase - .from('pictures') - .select('image_url') - .eq('id', pictureId) - .single(); - + const data = await fetchPictureById(pictureId); if (!data) return; if (imagePickerTarget === 'first') { diff --git a/packages/ui/src/pages/VideoPlayerPlayground.tsx b/packages/ui/src/pages/VideoPlayerPlayground.tsx deleted file mode 100644 index 2e037dec..00000000 --- a/packages/ui/src/pages/VideoPlayerPlayground.tsx +++ /dev/null @@ -1,657 +0,0 @@ -import { useState, useRef, useEffect } from "react"; -import { useLocation } from "react-router-dom"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import VideoCard from "@/components/VideoCard"; -// import MuxUploader from "@mux/mux-uploader-react"; -import { toast } from "sonner"; -import { supabase } from "@/integrations/supabase/client"; -import { useAuth } from "@/hooks/useAuth"; -import { Loader2, Upload, CheckCircle2, AlertCircle } from "lucide-react"; -import { MuxVideoMetadata, MuxTrack } from "@/types"; - -type UploadStatus = 'idle' | 'uploading' | 'processing' | 'ready' | 'error'; - -interface MuxAsset { - id: string; - status: 'preparing' | 'ready' | 'errored'; - duration?: number; - aspect_ratio?: string; - created_at?: string; - max_stored_resolution?: string; - max_stored_frame_rate?: number; - tracks?: MuxTrack[]; - playback_ids?: Array<{ - id: string; - policy: string; - }>; -} - -const VideoPlayerPlayground = () => { - const { user, session } = useAuth(); - const location = useLocation(); - const [videoUrl, setVideoUrl] = useState("https://files.vidstack.io/sprite-fight/720p.mp4"); - const [title, setTitle] = useState("Sprite Fight"); - const [description, setDescription] = useState(""); - const [thumbnailUrl, setThumbnailUrl] = useState(""); - const [previewKey, setPreviewKey] = useState(0); - - // Mux upload states - const [uploadStatus, setUploadStatus] = useState('idle'); - const [uploadId, setUploadId] = useState(null); - const [assetId, setAssetId] = useState(null); - const [playbackId, setPlaybackId] = useState(null); - const [uploadProgress, setUploadProgress] = useState(0); - const uploaderRef = useRef(null); - - // Handle incoming video file from CreationWizard - useEffect(() => { - const state = location.state as { videoFile?: File } | null; - if (state?.videoFile && user) { - const file = state.videoFile; - setTitle(file.name.replace(/\.[^/.]+$/, '')); // Remove extension - setDescription(`Uploaded video: ${file.name}`); - - // Automatically trigger upload - toast.info('Ready to upload: ' + file.name); - - // Note: The actual upload will be triggered when user drags/selects file in MuxUploader - // We can't programmatically upload to MuxUploader, so we just pre-fill the metadata - } - }, [location.state, user]); - - const handleUpdatePreview = () => { - if (!videoUrl.trim()) { - toast.error("Please enter a video URL"); - return; - } - setPreviewKey(prev => prev + 1); - toast.success("Preview updated!"); - }; - // Poll for asset creation - const pollForAsset = async (uploadId: string) => { - let attempts = 0; - const maxAttempts = 60; // Poll for up to 2 minutes - - const poll = async (): Promise => { - if (!session) return; - - try { - const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; - const response = await fetch( - `${supabaseUrl}/functions/v1/mux-proxy`, - { - method: 'POST', - headers: { - 'Authorization': `Bearer ${session.access_token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - action: 'get-upload', - uploadId, - }), - } - ); - - const result = await response.json(); - - if (result.success && result.data.asset_id) { - // Asset created, now get asset details - await getAssetDetails(result.data.asset_id); - return; - } - - attempts++; - if (attempts < maxAttempts) { - setTimeout(() => poll(), 2000); // Poll every 2 seconds - } else { - setUploadStatus('error'); - toast.error('Upload processing timed out'); - } - } catch (error) { - console.error('Error polling upload:', error); - setUploadStatus('error'); - } - }; - - await poll(); - }; - - // Get asset details including playback ID - const getAssetDetails = async (assetId: string, attempts = 0) => { - if (!session) return; - - const maxAttempts = 60; // Poll for up to 2 minutes - - try { - const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; - const response = await fetch( - `${supabaseUrl}/functions/v1/mux-proxy`, - { - method: 'POST', - headers: { - 'Authorization': `Bearer ${session.access_token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - action: 'get-asset', - assetId, - }), - } - ); - - const result = await response.json(); - - if (result.success) { - const asset = result.data; - console.log('Asset status:', asset.status, 'Attempt:', attempts + 1); - - // Check if asset is ready - if (asset.status === 'ready' && asset.playback_ids?.[0]) { - const playbackId = asset.playback_ids[0].id; - setAssetId(assetId); - setPlaybackId(playbackId); - setUploadStatus('ready'); - - // Set the video URL to Mux stream - const muxVideoUrl = `https://stream.mux.com/${playbackId}.m3u8`; - const thumbnailUrl = `https://image.mux.com/${playbackId}/thumbnail.jpg`; - - setVideoUrl(muxVideoUrl); - setThumbnailUrl(thumbnailUrl); - setPreviewKey(prev => prev + 1); - - // Auto-save to database with complete Mux metadata - await saveVideoToDatabase(assetId, playbackId, uploadId || '', asset); - - toast.success('Video is ready to play!'); - } else if (asset.status === 'preparing' && attempts < maxAttempts) { - // Still processing, continue polling - // Only show toast every 10 attempts to avoid spam - if (attempts % 10 === 0) { - toast.info(`Still processing... (${Math.round((attempts / maxAttempts) * 100)}%)`, { - duration: 2000, - }); - } - setTimeout(() => getAssetDetails(assetId, attempts + 1), 2000); - } else if (asset.status === 'errored') { - setUploadStatus('error'); - toast.error('Video processing failed'); - } else if (attempts >= maxAttempts) { - setUploadStatus('error'); - toast.error('Video processing timed out. It may still be processing - check your Mux dashboard.'); - } - } else { - throw new Error(result.error || 'Failed to get asset details'); - } - } catch (error) { - console.error('Error getting asset details:', error); - - // Retry a few times for network errors - if (attempts < 5) { - setTimeout(() => getAssetDetails(assetId, attempts + 1), 3000); - } else { - setUploadStatus('error'); - toast.error('Failed to get video details'); - } - } - }; - - const handleUploadStart = () => { - setUploadStatus('uploading'); - setUploadProgress(0); - toast.info('Upload started...'); - }; - - const handleUploadProgress = (event: any) => { - const detail = event?.detail; - if (detail && typeof detail === 'number') { - setUploadProgress(Math.round(detail * 100)); - } - }; - - const handleUploadSuccess = (event: any) => { - const detail = event?.detail; - console.log('Upload success:', detail); - setUploadStatus('processing'); - toast.success('Upload complete! Processing video...'); - - if (uploadId) { - pollForAsset(uploadId); - } - }; - - const handleUploadError = (event: any) => { - const detail = event?.detail; - console.error('Upload error:', detail); - setUploadStatus('error'); - toast.error('Upload failed. Please try again.'); - }; - - const resetUpload = () => { - setUploadStatus('idle'); - setUploadId(null); - setAssetId(null); - setPlaybackId(null); - setUploadProgress(0); - }; - - const saveVideoToDatabase = async (assetId: string, playbackId: string, uploadId: string, asset: MuxAsset) => { - if (!user) { - console.log('No user, skipping auto-save'); - return; - } - - try { - // Check if already saved - const { data: existing } = await supabase - .from('pictures') - .select('id') - .eq('meta->>mux_asset_id', assetId) - .maybeSingle(); - - if (existing) { - console.log('Video already saved to database'); - return; - } - - // Prepare comprehensive metadata from Mux asset - const metadata: MuxVideoMetadata = { - mux_upload_id: uploadId, - mux_asset_id: assetId, - mux_playback_id: playbackId, - duration: asset.duration ?? null, - aspect_ratio: asset.aspect_ratio ?? null, - created_at: asset.created_at ?? new Date().toISOString(), - status: asset.status, - max_stored_resolution: asset.max_stored_resolution ?? null, - max_stored_frame_rate: asset.max_stored_frame_rate ?? null, - tracks: asset.tracks ?? [], - }; - - const { error } = await supabase - .from('pictures') - .insert({ - user_id: user.id, - title: title || 'Uploaded Video', - description: description || null, - image_url: `https://stream.mux.com/${playbackId}.m3u8`, // For videos, image_url stores the HLS URL - thumbnail_url: `https://image.mux.com/${playbackId}/thumbnail.jpg`, - type: 'mux-video', // Mark as video - meta: metadata as any, - }); - - if (error) throw error; - - console.log('Video auto-saved to database!'); - toast.success('Video saved to database!'); - } catch (error) { - console.error('Error saving video:', error); - toast.error('Failed to save video to database'); - } - }; - - const saveToDatabase = async () => { - if (!user || !assetId || !playbackId) { - toast.error('Missing required information'); - return; - } - - try { - // Get latest asset details - const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; - const response = await fetch( - `${supabaseUrl}/functions/v1/mux-proxy`, - { - method: 'POST', - headers: { - 'Authorization': `Bearer ${session?.access_token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - action: 'get-asset', - assetId, - }), - } - ); - - const result = await response.json(); - if (result.success) { - await saveVideoToDatabase(assetId, playbackId, uploadId || '', result.data); - } - } catch (error) { - console.error('Error saving video:', error); - toast.error('Failed to save video'); - } - }; - - const exampleVideos = [ - { - name: "Sprite Fight (720p)", - url: "https://files.vidstack.io/sprite-fight/720p.mp4", - thumbnail: "https://files.vidstack.io/sprite-fight/poster.webp" - }, - { - name: "Sprite Fight (1080p)", - url: "https://files.vidstack.io/sprite-fight/1080p.mp4", - thumbnail: "https://files.vidstack.io/sprite-fight/poster.webp" - }, - { - name: "Big Buck Bunny", - url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", - thumbnail: "" - }, - { - name: "Elephant's Dream", - url: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", - thumbnail: "" - } - ]; - - const loadExample = (video: typeof exampleVideos[0]) => { - setVideoUrl(video.url); - setTitle(video.name); - setThumbnailUrl(video.thumbnail); - setPreviewKey(prev => prev + 1); - toast.success(`Loaded: ${video.name}`); - }; - - return ( -
-
-

Video Player Playground

-

- Upload videos with Mux or test the Vidstack player with custom URLs -

-
- -
- {/* Controls Panel */} - - - Video Settings - Upload or configure your video - - - - - Upload Video - Test with URL - - - - {!user && ( - - - - Please sign in to upload videos - - - )} - - {user && ( - <> -
- - setTitle(e.target.value)} - /> -
- -
- - setDescription(e.target.value)} - /> -
- -
- - -
- - {uploadStatus !== 'idle' && ( -
- {uploadStatus === 'uploading' && ( - - - - Uploading: {uploadProgress}% - - - )} - - {uploadStatus === 'processing' && ( - - - -
-

Processing video... This may take a few minutes.

-

- Mux is transcoding your video and creating HLS streams. - You'll be notified when it's ready! -

-
-
-
- )} - - {uploadStatus === 'ready' && ( - - - -
-

✅ Video ready and saved to database!

-

- Asset ID: {assetId} -

-
-
-
- )} - - {uploadStatus === 'error' && ( - - - - Upload failed. Please try again. - - - )} - -
- -
-
- )} - - )} -
- - -
- - setVideoUrl(e.target.value)} - /> -
- -
- - setTitle(e.target.value)} - /> -
- -
- - setDescription(e.target.value)} - /> -
- -
- - setThumbnailUrl(e.target.value)} - /> -
- - - -
- -
- {exampleVideos.map((video, index) => ( - - ))} -
-
-
-
-
-
- - {/* Preview Panel */} - - - Preview - See how your video will look - - - - - -
- - {/* Info Section */} - - - About This Playground - - -
-

Vidstack Player

-

- Uses @vidstack/react for modern, customizable video playback. -

-
    -
  • Supports multiple video formats (MP4, WebM, HLS)
  • -
  • Responsive design with mobile and desktop layouts
  • -
  • Built-in controls: play, pause, volume, fullscreen, and more
  • -
  • Keyboard shortcuts and accessibility support
  • -
  • Picture-in-picture mode
  • -
-
- -
-

Mux Upload

-

- Uses @mux/mux-uploader-react for professional video upload and streaming. -

-
    -
  • Drag & drop or click to upload
  • -
  • Automatic video processing and optimization
  • -
  • Generates HLS streams for adaptive bitrate playback
  • -
  • Automatic thumbnail generation
  • -
  • Progress tracking and error handling
  • -
-
- - {playbackId && ( -
-

Your Upload Details

-
-
- - - {assetId} - -
-
- - - {playbackId} - -
-
- - - https://stream.mux.com/{playbackId}.m3u8 - -
-
- - - https://image.mux.com/{playbackId}/thumbnail.jpg - -
-
- - - https://player.mux.com/{playbackId} - -
-
-
- )} -
-
-
- ); -}; - -export default VideoPlayerPlayground; - diff --git a/packages/ui/src/utils/collectionUtils.ts b/packages/ui/src/utils/collectionUtils.ts index 031256a6..807d22a2 100644 --- a/packages/ui/src/utils/collectionUtils.ts +++ b/packages/ui/src/utils/collectionUtils.ts @@ -1,54 +1,21 @@ -import { supabase } from '@/integrations/supabase/client'; +import { apiClient } from '@/lib/db'; /** * Filters out pictures that are only in private collections. - * Pictures that are: - * - Not in any collection: SHOWN - * - In at least one public collection: SHOWN - * - Only in private collections: HIDDEN + * Uses server API to check collection visibility. */ export async function filterPrivateCollectionPictures( pictures: T[] ): Promise { - const filteredPictures = await Promise.all( - pictures.map(async (picture) => { - // Check if picture is in any collections - const { data: collectionPictures, error: cpError } = await supabase - .from('collection_pictures') - .select('collection_id') - .eq('picture_id', picture.id); - - if (cpError) { - console.error('Error checking collections:', cpError); - return picture; // If error, include the picture - } - - // If not in any collection, show it - if (!collectionPictures || collectionPictures.length === 0) { - return picture; - } - - // Check if at least one collection is public - const collectionIds = collectionPictures.map(cp => cp.collection_id); - const { data: collections, error: collectionsError } = await supabase - .from('collections') - .select('is_public') - .in('id', collectionIds); - - if (collectionsError) { - console.error('Error checking collection privacy:', collectionsError); - return picture; // If error, include the picture - } - - // If at least one collection is public, show the picture - const hasPublicCollection = collections?.some(c => c.is_public); - - return hasPublicCollection ? picture : null; - }) - ); - - // Remove null values (pictures that are only in private collections) - return filteredPictures.filter((p): p is NonNullable => p !== null); + if (pictures.length === 0) return pictures; + try { + const ids = pictures.map(p => p.id).join(','); + const result = await apiClient<{ publicIds: string[] }>(`/api/collection-pictures/public-filter?ids=${ids}`); + const publicIds = new Set(result.publicIds); + return pictures.filter(p => publicIds.has(p.id)); + } catch { + return pictures; + } }