This commit is contained in:
lovebird 2026-04-09 20:00:53 +02:00
parent 56164fdb31
commit a20bb1b6e9
22 changed files with 188 additions and 449 deletions

View File

@ -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 (
<HelmetProvider>
<SWRConfig value={{ provider: () => new Map() }}>
<OidcProvider {...oidcConfig}>
<OidcProvider
userManager={userManager}
onSigninCallback={() => window.history.replaceState({}, document.title, window.location.pathname)}
>
<QueryClientProvider client={queryClient}>
<AuthProvider>
<LogProvider>

View File

@ -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<string, unknown>;
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<void>;
user: AuthUser | null;
/** Always null — Zitadel uses tokens, not Supabase sessions */
session: null;
roles: string[];
loading: boolean;
isAuthenticated: boolean;
signIn: () => Promise<void>;
signOut: () => Promise<void>;
/** @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<AuthContextType | undefined>(undefined);
// ─── Provider ─────────────────────────────────────────────────────────────────
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [roles, setRoles] = useState<string[]>([]);
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<string[]>([]);
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-<project-ref>-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 (
<AuthContext.Provider value={{
user,
session,
roles,
loading,
signUp,
signIn,
signInWithGithub,
signInWithGoogle,
resetPassword,
signOut,
}}>
{children}
</AuthContext.Provider>
);
return (
<AuthContext.Provider
value={{
user,
session: null,
roles,
loading: oidcAuth.isLoading || rolesLoading,
isAuthenticated: oidcAuth.isAuthenticated,
signIn: () => 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}
</AuthContext.Provider>
);
};
// ─── 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;
};

View File

@ -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) {

View File

@ -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 <T>(
export const serverUrl = import.meta.env.VITE_SERVER_IMAGE_API_URL || window.location.origin;
export const getAuthToken = async (): Promise<string | undefined> => {
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 */

View File

@ -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);

View File

@ -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 }),
});

View File

@ -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()}`, {

View File

@ -173,11 +173,9 @@ export function useChatEngine(namespace = 'chat') {
const headers: Record<string, string> = {};
// 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;

View File

@ -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<string | null> => {
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<Response> => {

View File

@ -26,8 +26,8 @@ export interface Campaign {
// ─── Auth helper ──────────────────────────────────────────────────────────────
async function authHeaders(contentType?: string): Promise<HeadersInit> {
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;

View File

@ -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<Category> & { 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<Category> & { parentId?:
};
export const updateCategory = async (id: string, updates: Partial<Category>) => {
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<Category>) =>
};
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}`;

View File

@ -53,8 +53,8 @@ export interface ContactGroup {
// ─── Auth helper ──────────────────────────────────────────────────────────────
async function authHeaders(contentType?: string): Promise<HeadersInit> {
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;

View File

@ -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<HeadersInit> {
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;

View File

@ -10,10 +10,8 @@ const API_BASE = import.meta.env.VITE_API_URL || '';
// ---------------------------------------------------------------------------
async function getAuthToken(): Promise<string | null> {
// 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<Response> {

View File

@ -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<string, string>) => {
// 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<WidgetTranslation, 'id' | 'created_at' | 'updated_at'>;
const authHeaders = async (): Promise<HeadersInit> => {
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;

View File

@ -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'

View File

@ -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<HTMLIFrameElement>(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 () => {

View File

@ -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<string, string> = {};
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}`, {

View File

@ -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 || '';

View File

@ -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'
};

View File

@ -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<any> => {
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<void> => {
// =============================================
const getAuthToken = async (): Promise<string> => {
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;
};

View File

@ -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) {