diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index d3c72914..1cbe6b83 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -7,7 +7,7 @@ import { queryClient } from "@/lib/queryClient"; import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom"; import { AuthProvider, useAuth } from "@/hooks/useAuth"; import { AuthProvider as OidcProvider } from "react-oidc-context"; -import { WebStorageStateStore } from "oidc-client-ts"; +import { userManager } from "@/lib/oidc"; import { LogProvider } from "@/contexts/LogContext"; @@ -274,27 +274,13 @@ const App = () => { initFormatDetection(); }, []); - const oidcConfig = { - authority: "https://auth.polymech.info", - client_id: "367440527605432321", - redirect_uri: window.location.origin + "/authz", // Where Zitadel sends the code back to - // Must appear in Zitadel app → Post Logout URIs (same allowlist pattern as redirect_uri; bare origin alone often fails). - post_logout_redirect_uri: window.location.origin + "/authz", - response_type: "code", - scope: "openid profile email", - loadUserInfo: true, // Specifically instruct the client to fetch /userinfo - // Store tokens in localStorage instead of sessionStorage so they survive new tabs - userStore: new WebStorageStateStore({ store: window.localStorage }), - onSigninCallback: () => { - // Clean up the URL after successful login - window.history.replaceState({}, document.title, window.location.pathname); - } - }; - return ( new Map() }}> - + window.history.replaceState({}, document.title, window.location.pathname)} + > diff --git a/packages/ui/src/hooks/useAuth.tsx b/packages/ui/src/hooks/useAuth.tsx index f0811c79..7b2c2661 100644 --- a/packages/ui/src/hooks/useAuth.tsx +++ b/packages/ui/src/hooks/useAuth.tsx @@ -1,310 +1,121 @@ -import { createContext, useContext, useEffect, useState, useRef } from 'react'; -import { User, Session } from '@supabase/supabase-js'; -import { supabase } from '@/integrations/supabase/client'; -import { useToast } from '@/hooks/use-toast'; -import { fetchUserRoles, notifyServerLogout } from '@/modules/user/client-user'; +import { createContext, useContext, useEffect, useState } from 'react'; +import { useAuth as useOidcAuth } from 'react-oidc-context'; +import { fetchUserRoles } from '@/modules/user/client-user'; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +export interface AuthUser { + id: string; // Zitadel numeric sub + email: string | undefined; + user_metadata: { + display_name?: string; + username?: string; + full_name?: string; + }; + app_metadata: Record; + aud: string; +} interface AuthContextType { - user: User | null; - session: Session | null; - roles: string[]; - loading: boolean; - signUp: (email: string, password: string, username: string, displayName: string) => Promise<{ data: any; error: any }>; - signIn: (email: string, password: string) => Promise<{ error: any }>; - signInWithGithub: () => Promise<{ error: any }>; - signInWithGoogle: () => Promise<{ error: any }>; - resetPassword: (email: string) => Promise<{ error: any }>; - signOut: () => Promise; + user: AuthUser | null; + /** Always null — Zitadel uses tokens, not Supabase sessions */ + session: null; + roles: string[]; + loading: boolean; + isAuthenticated: boolean; + signIn: () => Promise; + signOut: () => Promise; + /** @deprecated Use signIn() — redirects to Zitadel */ + signUp: (...args: any[]) => Promise<{ data: null; error: Error }>; + /** @deprecated Use signIn() — redirects to Zitadel */ + signInWithGithub: () => Promise<{ error: null }>; + /** @deprecated Use signIn() — redirects to Zitadel */ + signInWithGoogle: () => Promise<{ error: null }>; + /** @deprecated Reset password via Zitadel portal */ + resetPassword: (...args: any[]) => Promise<{ error: Error }>; } const AuthContext = createContext(undefined); +// ─── Provider ───────────────────────────────────────────────────────────────── + export const AuthProvider = ({ children }: { children: React.ReactNode }) => { - const [user, setUser] = useState(null); - const [session, setSession] = useState(null); - const [roles, setRoles] = useState([]); - const [loading, setLoading] = useState(true); - const loadingRef = useRef(true); // Track loading state for timeouts/callbacks without closure staleness - const { toast } = useToast(); + const oidcAuth = useOidcAuth(); + const [roles, setRoles] = useState([]); + const [rolesLoading, setRolesLoading] = useState(false); - useEffect(() => { - loadingRef.current = loading; - }, [loading]); + useEffect(() => { + if (oidcAuth.isLoading) return; - useEffect(() => { - let mounted = true; - let cleanup: (() => void) | undefined; + if (!oidcAuth.isAuthenticated || !oidcAuth.user) { + setRoles([]); + return; + } - // Simplified and robust auth initialization - const initAuth = async () => { - const startTime = Date.now(); - const MIN_LOADING_TIME = 500; + const sub = oidcAuth.user.profile.sub; - // 1. Setup listener *before* checking session to catch any immediate events - const { data: { subscription } } = supabase.auth.onAuthStateChange( - async (event, session) => { - if (mounted) { - setSession(session); - setUser(session?.user ?? null); - - if (session?.user) { - // OPTIMIZATION: Check for cached roles to unblock UI immediately - const cachedRoles = localStorage.getItem(`polymech-roles-${session.user.id}`); - if (cachedRoles) { - try { - const parsedRoles = JSON.parse(cachedRoles); - if (mounted) setRoles(parsedRoles); - // If we have cached roles, we can stop loading immediately (after min time) - // We will still fetch fresh roles in background - if (loadingRef.current) { - const elapsed = Date.now() - startTime; - if (elapsed < MIN_LOADING_TIME) { - await new Promise(r => setTimeout(r, MIN_LOADING_TIME - elapsed)); - } - if (mounted) setLoading(false); - } - } catch (e) { - console.warn('Failed to parse cached roles', e); - } - } - - // Fetch fresh roles independently - fetchUserRoles(session.user.id).then(roles => { - if (!mounted) return; - - // Update cache - localStorage.setItem(`polymech-roles-${session.user.id}`, JSON.stringify(roles)); - - // Compare with current roles to avoid unnecessary re-renders if possible? - // For now just set them. React handles strict equality checks on state setters usually, - // but arrays are new references. - // Let's just set it. - if (mounted) setRoles(roles); - - // If we were still loading (no cache), stop loading now - if (loadingRef.current) { - if (mounted) setLoading(false); - } - }).catch(err => { - console.error('Error fetching user roles:', err); - // If we were still loading and failed, we should stop loading (roles=[], guest?) - if (loadingRef.current && !cachedRoles) { - if (mounted) setLoading(false); - } - }); - - } else { - if (mounted) { + setRolesLoading(true); + fetchUserRoles(sub) + .then(r => setRoles(r as string[])) + .catch(err => { + console.error('[AuthProvider] Failed to fetch roles:', err); setRoles([]); - if (loadingRef.current) setLoading(false); - } - } + }) + .finally(() => setRolesLoading(false)); + }, [oidcAuth.isAuthenticated, oidcAuth.isLoading]); + + const user: AuthUser | null = oidcAuth.user + ? { + id: oidcAuth.user.profile.sub, + email: oidcAuth.user.profile.email, + user_metadata: { + display_name: oidcAuth.user.profile.name, + full_name: oidcAuth.user.profile.name, + username: oidcAuth.user.profile.preferred_username, + }, + app_metadata: {}, + aud: String(oidcAuth.user.profile.aud ?? ''), } - } - ); + : null; - // 2. Trigger initial check, but don't block narrowly on it - // The onAuthStateChange often fires INITIAL_SESSION immediately. - - const safetyTimeout = setTimeout(() => { - if (mounted && loadingRef.current) { - console.warn('Auth initialization safety timeout (5s) - forcing app load'); - setLoading(false); - } - }, 5000); - - try { - const { error } = await supabase.auth.getSession(); - if (error) throw error; - } catch (err) { - console.error("getSession failed or timed out", err); - // If this fails, the safety timeout or event listener (with null session) should handle it - } - - return () => { - clearTimeout(safetyTimeout); - subscription.unsubscribe(); - }; - }; - - initAuth().then(c => { cleanup = c; }); - - return () => { - mounted = false; - if (cleanup) cleanup(); - }; - }, []); - - const signUp = async (email: string, password: string, username: string, displayName: string) => { - const redirectUrl = `${window.location.origin}/`; - - const { data, error } = await supabase.auth.signUp({ - email, - password, - options: { - emailRedirectTo: redirectUrl, - data: { - username, - display_name: displayName - } - } - }); - - if (error) { - toast({ - title: "Sign up failed", - description: error.message, - variant: "destructive", - }); - } else { - toast({ - title: "Check your email", - description: "Please check your email to confirm your account.", - }); - } - - return { data, error }; - }; - - const signIn = async (email: string, password: string) => { - const { error } = await supabase.auth.signInWithPassword({ - email, - password, - }); - - if (error) { - toast({ - title: "Sign in failed", - description: error.message, - variant: "destructive", - }); - } - - return { error }; - }; - - const signInWithGithub = async () => { - const { error } = await supabase.auth.signInWithOAuth({ - provider: 'github', - options: { - redirectTo: `${window.location.origin}/` - } - }); - - if (error) { - toast({ - title: "Sign in failed", - description: error.message, - variant: "destructive", - }); - } - - return { error }; - }; - - const signInWithGoogle = async () => { - const redirectTo = `${window.location.origin}/`; - const { error } = await supabase.auth.signInWithOAuth({ - provider: 'google', - options: { - redirectTo - } - }); - - if (error) { - toast({ - title: "Sign in failed", - description: error.message, - variant: "destructive", - }); - } - - return { error }; - }; - - const resetPassword = async (email: string) => { - const baseUrl = import.meta.env.VITE_REDIRECT_URL || window.location.origin; - const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo: `${baseUrl}/auth/update-password`, - }); - - if (error) { - toast({ - title: 'Reset failed', - description: error.message, - variant: 'destructive', - }); - } else { - toast({ - title: 'Check your email', - description: 'A password reset link has been sent to your email.', - }); - } - - return { error }; - }; - - const signOut = async () => { - // 1. Grab current token before signing out (we need it to notify the server) - let token: string | null = null; - try { - const { data } = await supabase.auth.getSession(); - token = data?.session?.access_token ?? null; - } catch { /* ignore — we'll still clean up */ } - - // 2. Notify server to flush its auth cache (fire-and-forget, don't block on errors) - if (token) { - notifyServerLogout(token); - } - - // 3. Attempt Supabase signOut (may fail with 503 if auth service is down) - try { - await supabase.auth.signOut(); - } catch (err) { - console.warn('Supabase signOut failed (forcing local cleanup):', err); - } - - // 4. Force-clear localStorage auth token regardless of Supabase response - // The key format is: sb--auth-token - for (const key of Object.keys(localStorage)) { - if (key.startsWith('sb-') && key.endsWith('-auth-token')) { - localStorage.removeItem(key); - } - } - - // 5. Clear cached roles - if (user?.id) { - localStorage.removeItem(`polymech-roles-${user.id}`); - } - - // 6. Reset React state — user is now logged out - setUser(null); - setSession(null); - setRoles([]); - }; - - return ( - - {children} - - ); + return ( + oidcAuth.signinRedirect(), + signOut: () => oidcAuth.signoutRedirect(), + 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 }; + }, + resetPassword: async () => ({ + error: new Error('Reset password via the Zitadel portal'), + }), + }} + > + {children} + + ); }; +// ─── Hook ───────────────────────────────────────────────────────────────────── + export const useAuth = () => { - const context = useContext(AuthContext); - if (context === undefined) { - throw new Error('useAuth must be used within an AuthProvider'); - } - return context; + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; }; diff --git a/packages/ui/src/hooks/useFeedData.ts b/packages/ui/src/hooks/useFeedData.ts index 1f320070..3d13cb4b 100644 --- a/packages/ui/src/hooks/useFeedData.ts +++ b/packages/ui/src/hooks/useFeedData.ts @@ -5,7 +5,7 @@ import { useFeedCache } from '@/contexts/FeedCacheContext'; import { augmentFeedPosts, FeedPost } from '@/modules/posts/client-posts'; import { searchContent, searchContentStream } from '@/modules/search/client-search'; import { getCurrentLang } from '@/i18n'; -import { fetchWithDeduplication } from '@/lib/db'; +import { fetchWithDeduplication, getAuthToken } from '@/lib/db'; import { fetchFeed } from '@/modules/feed/client-feed'; const { supabase } = await import('@/integrations/supabase/client'); @@ -120,8 +120,7 @@ export const useFeedData = ({ try { // 0. Search source — use streaming API if (source === 'search' && sourceId) { - const client = supabaseClient || supabase; - const { data: { session } } = await client.auth.getSession(); + const session = { access_token: await getAuthToken() }; // Close any existing stream if (streamRef.current) { diff --git a/packages/ui/src/lib/db.ts b/packages/ui/src/lib/db.ts index 0948687d..3e844eb3 100644 --- a/packages/ui/src/lib/db.ts +++ b/packages/ui/src/lib/db.ts @@ -1,5 +1,6 @@ import { queryClient } from './queryClient'; import { supabase as defaultSupabase } from "@/integrations/supabase/client"; +import { userManager } from './oidc'; import { SupabaseClient } from "@supabase/supabase-js"; @@ -72,8 +73,8 @@ export const fetchWithDeduplication = async ( export const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin; export const getAuthToken = async (): Promise => { - const { data: sessionData } = await defaultSupabase.auth.getSession(); - return sessionData?.session?.access_token; + const user = await userManager.getUser(); + return user?.access_token ?? undefined; }; /** Helper function to get authorization headers */ diff --git a/packages/ui/src/lib/image-router.ts b/packages/ui/src/lib/image-router.ts index b99f9207..b844e438 100644 --- a/packages/ui/src/lib/image-router.ts +++ b/packages/ui/src/lib/image-router.ts @@ -10,7 +10,7 @@ */ import { createImage as createImageGoogle, editImage as editImageGoogle } from '@/image-api'; -import { createImageWithReplicate, editImageWithReplicate } from '@/lib/replicate'; +//import { createImageWithReplicate, editImageWithReplicate } from '@/lib/replicate'; import { createImageWithBria, editImageWithBria } from '@/lib/bria'; import { createImageWithAimlApi, editImageWithAimlApi } from '@/lib/aimlapi'; @@ -329,9 +329,6 @@ export const createImage = async ( case 'google': return await createImageGoogle(prompt, modelName, apiKey, aspectRatio, resolution, enableSearchGrounding, enableImageSearch); - case 'replicate': - return await createImageWithReplicate(prompt, modelName, apiKey); - case 'bria': return await createImageWithBria(prompt, modelName, apiKey); @@ -382,9 +379,6 @@ export const editImage = async ( case 'google': return await editImageGoogle(prompt, imageFiles, modelName, apiKey, aspectRatio, resolution, enableSearchGrounding, enableImageSearch); - case 'replicate': - return await editImageWithReplicate(prompt, imageFiles, modelName, apiKey); - case 'bria': return await editImageWithBria(prompt, imageFiles, modelName, apiKey); diff --git a/packages/ui/src/lib/oidc.ts b/packages/ui/src/lib/oidc.ts new file mode 100644 index 00000000..be456cf3 --- /dev/null +++ b/packages/ui/src/lib/oidc.ts @@ -0,0 +1,15 @@ +import { UserManager, WebStorageStateStore } from 'oidc-client-ts'; + +const authority = import.meta.env.VITE_ZITADEL_AUTHORITY as string; +const client_id = import.meta.env.VITE_ZITADEL_CLIENT_ID as string; + +export const userManager = new UserManager({ + authority, + client_id, + redirect_uri: window.location.origin + '/authz', + post_logout_redirect_uri: window.location.origin + '/authz', + response_type: 'code', + scope: 'openid profile email', + loadUserInfo: true, + userStore: new WebStorageStateStore({ store: window.localStorage }), +}); diff --git a/packages/ui/src/modules/ai/searchTools.ts b/packages/ui/src/modules/ai/searchTools.ts index abc21b35..6021456a 100644 --- a/packages/ui/src/modules/ai/searchTools.ts +++ b/packages/ui/src/modules/ai/searchTools.ts @@ -519,9 +519,8 @@ export const markdownScraperTool = (addLog: LogFunction = defaultLog): RunnableT try { addLog('info', '[SEARCH-TOOLS] markdown_scraper called', { url: args.url }); - const { supabase } = await import('@/integrations/supabase/client'); - const session = await supabase.auth.getSession(); - const token = session.data.session?.access_token; + const { getAuthToken } = await import('@/lib/db'); + const token = await getAuthToken(); const res = await fetch('/api/scrape/markdown', { method: 'POST', @@ -782,9 +781,8 @@ export const webSearchTool = ( num: String(num), }); - const { supabase } = await import('@/integrations/supabase/client'); - const session = await supabase.auth.getSession(); - const token = session.data.session?.access_token; + const { getAuthToken } = await import('@/lib/db'); + const token = await getAuthToken(); // Call server-side proxy (avoids CORS, hides API key) const res = await fetch(`/api/serpapi/search?${params.toString()}`, { diff --git a/packages/ui/src/modules/ai/useChatEngine.ts b/packages/ui/src/modules/ai/useChatEngine.ts index 3956be2f..b11b44c6 100644 --- a/packages/ui/src/modules/ai/useChatEngine.ts +++ b/packages/ui/src/modules/ai/useChatEngine.ts @@ -173,11 +173,9 @@ export function useChatEngine(namespace = 'chat') { const headers: Record = {}; // Reuse the auth pattern from useChatEngine's existing getClient try { - const { supabase } = await import('@/integrations/supabase/client'); - const { data } = await supabase.auth.getSession(); - if (data?.session?.access_token) { - headers['Authorization'] = `Bearer ${data.session.access_token}`; - } + const { getAuthToken } = await import('@/lib/db'); + const token = await getAuthToken(); + if (token) headers['Authorization'] = `Bearer ${token}`; } catch { } const apiBase = import.meta.env.VITE_SERVER_IMAGE_API_URL || ''; const res = await fetch(`${apiBase}/api/vfs/read/${mount}/${clean}`, { headers }); @@ -257,9 +255,8 @@ export function useChatEngine(namespace = 'chat') { // Read from local session for proxy backend auth let token: string | undefined = undefined; try { - const { supabase } = await import('@/integrations/supabase/client'); - const { data } = await supabase.auth.getSession(); - token = data?.session?.access_token; + const { getAuthToken } = await import('@/lib/db'); + token = await getAuthToken(); } catch { } if (!token) return null; diff --git a/packages/ui/src/modules/ai/vfsTools.ts b/packages/ui/src/modules/ai/vfsTools.ts index 4ca1a357..e93e2226 100644 --- a/packages/ui/src/modules/ai/vfsTools.ts +++ b/packages/ui/src/modules/ai/vfsTools.ts @@ -7,7 +7,7 @@ import { z } from 'zod'; import type { RunnableToolFunctionWithParse } from 'openai/lib/RunnableFunction'; -import { supabase } from '@/integrations/supabase/client'; +import { getAuthToken } from '@/lib/db'; type LogFunction = (level: 'info' | 'warn' | 'error' | 'debug', message: string, data?: any) => void; const defaultLog: LogFunction = (level, message, data) => console.log(`[FS-TOOLS][${level}] ${message}`, data ?? ''); @@ -17,8 +17,7 @@ const default_mount = 'home'; // ── Auth helper ────────────────────────────────────────────────────────── const getToken = async (): Promise => { - const { data } = await supabase.auth.getSession(); - return data?.session?.access_token || null; + return (await getAuthToken()) ?? null; }; const vfsFetch = async (method: string, op: string, subpath?: string, body?: string): Promise => { diff --git a/packages/ui/src/modules/campaigns/client-campaigns.ts b/packages/ui/src/modules/campaigns/client-campaigns.ts index bd773bc9..9208ff76 100644 --- a/packages/ui/src/modules/campaigns/client-campaigns.ts +++ b/packages/ui/src/modules/campaigns/client-campaigns.ts @@ -26,8 +26,8 @@ export interface Campaign { // ─── Auth helper ────────────────────────────────────────────────────────────── async function authHeaders(contentType?: string): Promise { - const { data } = await defaultSupabase.auth.getSession(); - const token = data.session?.access_token; + const { getAuthToken } = await import('@/lib/db'); + const token = await getAuthToken(); const h: HeadersInit = {}; if (token) h['Authorization'] = `Bearer ${token}`; if (contentType) h['Content-Type'] = contentType; diff --git a/packages/ui/src/modules/categories/client-categories.ts b/packages/ui/src/modules/categories/client-categories.ts index 7a831a57..965c30c9 100644 --- a/packages/ui/src/modules/categories/client-categories.ts +++ b/packages/ui/src/modules/categories/client-categories.ts @@ -1,5 +1,4 @@ -import { supabase as defaultSupabase } from "@/integrations/supabase/client"; -import { fetchWithDeduplication } from "@/lib/db"; +import { fetchWithDeduplication, getAuthToken } from "@/lib/db"; import { getCurrentLang } from '@/i18n'; // --- Category Management --- @@ -50,8 +49,7 @@ export const fetchCategories = async (options?: { parentSlug?: string; includeCh export const createCategory = async (category: Partial & { parentId?: string; relationType?: string }) => { - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = `Bearer ${token}`; @@ -65,8 +63,7 @@ export const createCategory = async (category: Partial & { parentId?: }; export const updateCategory = async (id: string, updates: Partial) => { - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = `Bearer ${token}`; @@ -80,8 +77,7 @@ export const updateCategory = async (id: string, updates: Partial) => }; export const deleteCategory = async (id: string) => { - 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 c5690ebb..2cbd37fe 100644 --- a/packages/ui/src/modules/contacts/client-contacts.ts +++ b/packages/ui/src/modules/contacts/client-contacts.ts @@ -53,8 +53,8 @@ export interface ContactGroup { // ─── Auth helper ────────────────────────────────────────────────────────────── async function authHeaders(contentType?: string): Promise { - const { data } = await defaultSupabase.auth.getSession(); - const token = data.session?.access_token; + const { getAuthToken } = await import('@/lib/db'); + const token = await getAuthToken(); const h: HeadersInit = {}; if (token) h['Authorization'] = `Bearer ${token}`; if (contentType) h['Content-Type'] = contentType; diff --git a/packages/ui/src/modules/contacts/client-mailboxes.ts b/packages/ui/src/modules/contacts/client-mailboxes.ts index e90c3dd4..cc0d5310 100644 --- a/packages/ui/src/modules/contacts/client-mailboxes.ts +++ b/packages/ui/src/modules/contacts/client-mailboxes.ts @@ -9,8 +9,8 @@ const SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL || ''; const API_BASE = '/api/contacts/mailboxes'; async function authHeaders(contentType?: string): Promise { - const { data } = await defaultSupabase.auth.getSession(); - const token = data.session?.access_token; + const { getAuthToken } = await import('@/lib/db'); + const token = await getAuthToken(); const h: HeadersInit = {}; if (token) h['Authorization'] = `Bearer ${token}`; if (contentType) h['Content-Type'] = contentType; diff --git a/packages/ui/src/modules/ecommerce/client-ecommerce.ts b/packages/ui/src/modules/ecommerce/client-ecommerce.ts index 11ce0d8e..99e83dae 100644 --- a/packages/ui/src/modules/ecommerce/client-ecommerce.ts +++ b/packages/ui/src/modules/ecommerce/client-ecommerce.ts @@ -10,10 +10,8 @@ const API_BASE = import.meta.env.VITE_API_URL || ''; // --------------------------------------------------------------------------- async function getAuthToken(): Promise { - // Dynamically import to avoid circular deps - const { supabase } = await import('@/integrations/supabase/client'); - const { data } = await supabase.auth.getSession(); - return data.session?.access_token || null; + const { getAuthToken: getZitadelToken } = await import('@/lib/db'); + return (await getZitadelToken()) ?? null; } async function authFetch(path: string, options: RequestInit = {}): Promise { diff --git a/packages/ui/src/modules/i18n/client-i18n.ts b/packages/ui/src/modules/i18n/client-i18n.ts index f7ce1f53..f44f4b17 100644 --- a/packages/ui/src/modules/i18n/client-i18n.ts +++ b/packages/ui/src/modules/i18n/client-i18n.ts @@ -1,6 +1,5 @@ -import { supabase as defaultSupabase } from "@/integrations/supabase/client"; import { z } from "zod"; -import { fetchWithDeduplication, invalidateCache } from "@/lib/db"; +import { fetchWithDeduplication, invalidateCache, getAuthToken } from "@/lib/db"; // --- i18n --- @@ -25,8 +24,7 @@ export interface Glossary { export const translateText = async (text: string, srcLang: string, dstLang: string, glossaryId?: string, format?: 'text' | 'markdown') => { // POST /api/i18n/translate - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = { 'Content-Type': 'application/json' }; @@ -64,8 +62,7 @@ export const fetchGlossaries = async () => { export const createGlossary = async (name: string, srcLang: string, dstLang: string, entries: Record) => { // POST /api/i18n/glossaries - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = { 'Content-Type': 'application/json' }; @@ -93,8 +90,7 @@ export const createGlossary = async (name: string, srcLang: string, dstLang: str export const deleteGlossary = async (id: string) => { // DELETE /api/i18n/glossaries/:id - 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}`; @@ -171,8 +167,7 @@ export interface WidgetTranslation { export type WidgetTranslationInput = Omit; const authHeaders = async (): Promise => { - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = `Bearer ${token}`; return headers; diff --git a/packages/ui/src/modules/layout/client-layouts.ts b/packages/ui/src/modules/layout/client-layouts.ts index 306e5a0e..e7ad7321 100644 --- a/packages/ui/src/modules/layout/client-layouts.ts +++ b/packages/ui/src/modules/layout/client-layouts.ts @@ -1,11 +1,8 @@ -import { supabase as defaultSupabase } from "@/integrations/supabase/client"; import { SupabaseClient } from "@supabase/supabase-js"; -import { fetchWithDeduplication } from "@/lib/db"; +import { fetchWithDeduplication, getAuthToken } from "@/lib/db"; export const createLayout = async (layoutData: any, client?: SupabaseClient) => { - const supabase = client || defaultSupabase; - const { data: sessionData } = await supabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = { 'Content-Type': 'application/json' @@ -28,13 +25,8 @@ export const createLayout = async (layoutData: any, client?: SupabaseClient) => export const getLayout = async (layoutId: string, client?: SupabaseClient) => { const key = `layout-${layoutId}`; return fetchWithDeduplication(key, async () => { - const supabase = client || defaultSupabase; - const { data: sessionData } = await supabase.auth.getSession(); - const token = sessionData.session?.access_token; - - const headers: HeadersInit = { - 'Content-Type': 'application/json' - }; + const token = await getAuthToken(); + const headers: HeadersInit = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = `Bearer ${token}`; const res = await fetch(`/api/layouts/${layoutId}`, { @@ -61,13 +53,8 @@ export const getLayouts = async (filters?: { type?: string, visibility?: string, const key = `layouts-${params.toString()}`; return fetchWithDeduplication(key, async () => { - const supabase = client || defaultSupabase; - const { data: sessionData } = await supabase.auth.getSession(); - const token = sessionData.session?.access_token; - - const headers: HeadersInit = { - 'Content-Type': 'application/json' - }; + const token = await getAuthToken(); + const headers: HeadersInit = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = `Bearer ${token}`; const res = await fetch(`/api/layouts?${params.toString()}`, { @@ -86,8 +73,7 @@ export const getLayouts = async (filters?: { type?: string, visibility?: string, }; export const updateLayoutMeta = async (layoutId: string, metaUpdates: any) => { // Fetch current layout to merge meta - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = `Bearer ${token}`; @@ -110,9 +96,7 @@ export const updateLayoutMeta = async (layoutId: string, metaUpdates: any) => { return await updateRes.json(); }; export const updateLayout = async (layoutId: string, layoutData: any, client?: SupabaseClient) => { - const supabase = client || defaultSupabase; - const { data: sessionData } = await supabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = { 'Content-Type': 'application/json' @@ -134,9 +118,7 @@ export const updateLayout = async (layoutId: string, layoutData: any, client?: S export const deleteLayout = async (layoutId: string, client?: SupabaseClient) => { - const supabase = client || defaultSupabase; - const { data: sessionData } = await supabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = { 'Content-Type': 'application/json' diff --git a/packages/ui/src/modules/pages/editor/hooks/useEmailActions.ts b/packages/ui/src/modules/pages/editor/hooks/useEmailActions.ts index 95fe7dd2..41fd9e7e 100644 --- a/packages/ui/src/modules/pages/editor/hooks/useEmailActions.ts +++ b/packages/ui/src/modules/pages/editor/hooks/useEmailActions.ts @@ -1,4 +1,5 @@ import { useState, useEffect, useRef } from "react"; +import { getAuthToken } from "@/lib/db"; import { toast } from "sonner"; import { translate, getCurrentLang } from "@/i18n"; @@ -21,11 +22,7 @@ export function useEmailActions({ page, orgSlug }: UseEmailActionsParams) { const iframeRef = useRef(null); useEffect(() => { - import("@/integrations/supabase/client").then(({ supabase }) => { - supabase.auth.getSession().then(({ data: { session } }) => { - setAuthToken(session?.access_token || null); - }); - }); + getAuthToken().then(token => setAuthToken(token ?? null)); }, []); const handleSendEmail = async () => { diff --git a/packages/ui/src/modules/posts/client-posts.ts b/packages/ui/src/modules/posts/client-posts.ts index 9ef69317..898ec2f2 100644 --- a/packages/ui/src/modules/posts/client-posts.ts +++ b/packages/ui/src/modules/posts/client-posts.ts @@ -2,7 +2,7 @@ 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 } from "@/lib/db"; +import { fetchWithDeduplication, getAuthToken } from "@/lib/db"; export interface FeedPost { id: string; // Post ID @@ -40,13 +40,9 @@ export const fetchPostDetailsAPI = async (id: string, options: { sizes?: string, // Let's assume we need to handle auth here or use a helper that does. // To keep it simple for now, we'll import `supabase` and get session. - const { supabase } = await import('@/integrations/supabase/client'); - 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}`; const res = await fetch(url, { headers }); if (!res.ok) { @@ -95,9 +91,7 @@ export const fetchFullPost = async (postId: string, client?: SupabaseClient) => }; export const deletePost = async (id: string, client?: SupabaseClient) => { - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; - + const token = await getAuthToken(); if (!token) throw new Error('No active session'); const response = await fetch(`/api/posts/${id}`, { @@ -114,9 +108,7 @@ export const deletePost = async (id: string, client?: SupabaseClient) => { return await response.json(); }; export const createPost = async (postData: { title: string, description?: string, settings?: any, meta?: any }) => { - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; - + const token = await getAuthToken(); if (!token) throw new Error('No active session'); const response = await fetch(`/api/posts`, { @@ -136,9 +128,7 @@ export const createPost = async (postData: { title: string, description?: string }; export const updatePostDetails = async (postId: string, updates: { title?: string, description?: string, settings?: any, meta?: any }, client?: SupabaseClient) => { - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; - + const token = await getAuthToken(); if (!token) throw new Error('No active session'); const response = await fetch(`/api/posts/${postId}`, { @@ -255,9 +245,7 @@ export const augmentFeedPosts = (posts: any[]): FeedPost[] => { }; export const updatePostMeta = async (postId: string, meta: any, client?: SupabaseClient) => { - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; - + const token = await getAuthToken(); if (!token) throw new Error('No active session'); const response = await fetch(`/api/posts/${postId}`, { diff --git a/packages/ui/src/modules/posts/client-videos.ts b/packages/ui/src/modules/posts/client-videos.ts index d8375849..5d9811f8 100644 --- a/packages/ui/src/modules/posts/client-videos.ts +++ b/packages/ui/src/modules/posts/client-videos.ts @@ -1,10 +1,4 @@ -import { supabase } from "@/integrations/supabase/client"; - -/** Helper to get auth token */ -const getAuthToken = async () => { - const { data: sessionData } = await supabase.auth.getSession(); - return sessionData.session?.access_token; -}; +import { getAuthToken } from "@/lib/db"; const API_BASE = import.meta.env.VITE_SERVER_IMAGE_API_URL || ''; diff --git a/packages/ui/src/modules/types/client-types.ts b/packages/ui/src/modules/types/client-types.ts index 0826f6ff..cb8b0341 100644 --- a/packages/ui/src/modules/types/client-types.ts +++ b/packages/ui/src/modules/types/client-types.ts @@ -1,5 +1,4 @@ -import { supabase } from "@/integrations/supabase/client"; -import { fetchWithDeduplication, invalidateCache } from "@/lib/db"; +import { fetchWithDeduplication, invalidateCache, getAuthToken } from "@/lib/db"; export interface TypeDefinition { id: string; @@ -43,8 +42,7 @@ export const fetchTypes = async (options?: { if (options?.parentTypeId) params.append('parentTypeId', options.parentTypeId); if (options?.visibility) params.append('visibility', options.visibility); - const { data: sessionData } = await supabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = {}; if (token) headers['Authorization'] = `Bearer ${token}`; @@ -59,8 +57,7 @@ export const fetchTypes = async (options?: { export const fetchTypeById = async (id: string) => { const key = `type-${id}`; return fetchWithDeduplication(key, async () => { - const { data: sessionData } = await supabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = {}; if (token) headers['Authorization'] = `Bearer ${token}`; @@ -81,8 +78,7 @@ export const createType = async (typeData: any) => { // Client side transaction is not easy. // Let's use the API endpoint: POST /api/types - const { data: sessionData } = await supabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = { 'Content-Type': 'application/json' }; @@ -106,8 +102,7 @@ export const createType = async (typeData: any) => { }; export const updateType = async (id: string, updates: any) => { - const { data: sessionData } = await supabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = { 'Content-Type': 'application/json' }; @@ -132,8 +127,7 @@ export const updateType = async (id: string, updates: any) => { }; export const deleteType = async (id: string) => { - const { data: sessionData } = await supabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getAuthToken(); const headers: HeadersInit = { 'Content-Type': 'application/json' }; diff --git a/packages/ui/src/modules/user/client-user.ts b/packages/ui/src/modules/user/client-user.ts index 8e6f1a5d..6985b4f0 100644 --- a/packages/ui/src/modules/user/client-user.ts +++ b/packages/ui/src/modules/user/client-user.ts @@ -1,7 +1,7 @@ import { supabase as defaultSupabase } from "@/integrations/supabase/client"; import { UserProfile } from "@/modules/posts/views/types"; import { SupabaseClient } from "@supabase/supabase-js"; -import { fetchWithDeduplication } from "@/lib/db"; +import { fetchWithDeduplication, getAuthToken as getZitadelToken } from "@/lib/db"; const serverUrl = (path: string) => { const baseUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin; @@ -319,8 +319,7 @@ export const updateProfileAPI = async (profileData: { avatar_url?: string | null; settings?: any; }): Promise => { - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getZitadelToken(); if (!token) throw new Error('Not authenticated'); const res = await fetch(serverUrl('/api/profile'), { @@ -346,8 +345,7 @@ export const updateUserEmail = async (newEmail: string): Promise => { // ============================================= const getAuthToken = async (): Promise => { - const { data: sessionData } = await defaultSupabase.auth.getSession(); - const token = sessionData.session?.access_token; + const token = await getZitadelToken(); if (!token) throw new Error('Not authenticated'); return token; }; diff --git a/packages/ui/src/utils/uploadUtils.ts b/packages/ui/src/utils/uploadUtils.ts index 3cffef56..8d8ecf38 100644 --- a/packages/ui/src/utils/uploadUtils.ts +++ b/packages/ui/src/utils/uploadUtils.ts @@ -2,7 +2,7 @@ * Upload Utility Functions */ -import { supabase } from '@/integrations/supabase/client'; +import { getAuthToken } from '@/lib/db'; // Helper to upload internal video export const uploadInternalVideo = async ( @@ -10,8 +10,7 @@ export const uploadInternalVideo = async ( userId: string, onProgress?: (progress: number) => void ): Promise<{ dbId: string; thumbnailUrl: string; meta: any }> => { - // Get auth token before creating the XHR - const { data: { session } } = await supabase.auth.getSession(); + const token = await getAuthToken(); return new Promise((resolve, reject) => { const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL; @@ -21,9 +20,7 @@ export const uploadInternalVideo = async ( const xhr = new XMLHttpRequest(); xhr.open('POST', `${serverUrl}/api/videos/upload?userId=${userId}&title=${encodeURIComponent(title)}&preset=original`); - if (session?.access_token) { - xhr.setRequestHeader('Authorization', `Bearer ${session.access_token}`); - } + if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`); xhr.upload.onprogress = (e) => { if (e.lengthComputable && onProgress) {