diff --git a/web/src/backtest/api.ts b/web/src/backtest/api.ts index 10ceeb8..fa8aeb7 100644 --- a/web/src/backtest/api.ts +++ b/web/src/backtest/api.ts @@ -1,13 +1,9 @@ -import { supabase } from '../lib/supabaseClient'; +import { getPlatformAccessToken } from '../lib/authSession'; import type { BacktestRequestPayload, BacktestResult } from './types'; import { tradingRuntime } from '../lib/runtime'; export const runBacktestApi = async (payload: BacktestRequestPayload): Promise => { - const { data: sessionData } = await supabase.auth.getSession(); - const accessToken = sessionData.session?.access_token; - if (!accessToken) { - throw new Error('Not authenticated'); - } + const accessToken = await getPlatformAccessToken(); const apiUrl = tradingRuntime.tradingApiUrl; const response = await fetch(`${apiUrl}/api/backtest/run`, { diff --git a/web/src/backtest/flags.ts b/web/src/backtest/flags.ts index 1c553e5..17d9229 100644 --- a/web/src/backtest/flags.ts +++ b/web/src/backtest/flags.ts @@ -1,4 +1,4 @@ -import { supabase } from '../lib/supabaseClient'; +import { getPlatformAccessToken } from '../lib/authSession'; import { tradingRuntime } from '../lib/runtime'; export interface BacktestRuntimeFlags { @@ -44,8 +44,7 @@ export const loadBacktestRuntimeFlags = async (): Promise return runtimeFlagsCache; } - const sessionData = await supabase.auth.getSession(); - const accessToken = sessionData?.data?.session?.access_token; + const accessToken = await getPlatformAccessToken().catch(() => null); if (!accessToken) { const fallback = { enableBacktest: false, customerEnabled: false }; runtimeFlagsCache = fallback; diff --git a/web/src/components/AuthContext.dom.test.tsx b/web/src/components/AuthContext.dom.test.tsx index 894f7f3..3b2db66 100644 --- a/web/src/components/AuthContext.dom.test.tsx +++ b/web/src/components/AuthContext.dom.test.tsx @@ -5,14 +5,12 @@ import { render, screen, waitFor } from '@testing-library/react'; import { AuthProvider, useAuth } from './AuthContext'; const { - getSessionMock, - signOutMock, + ensurePlatformSessionMock, tradingAuthState, fetchCurrentUserProfileMock, fetchTradeProfilesMock } = vi.hoisted(() => ({ - getSessionMock: vi.fn(), - signOutMock: vi.fn(), + ensurePlatformSessionMock: vi.fn(), tradingAuthState: { user: { id: 'user-1', email: 'sarah@example.com', role: 'admin', name: 'Sarah Algo' } as any, isLoading: false, @@ -27,13 +25,9 @@ vi.mock('../lib/tradingAuth', () => ({ useTradingAuth: () => tradingAuthState })); -vi.mock('../lib/supabaseClient', () => ({ - supabase: { - auth: { - getSession: getSessionMock, - signOut: signOutMock - } - } +vi.mock('../lib/authSession', () => ({ + ensurePlatformSession: ensurePlatformSessionMock, + clearPlatformSession: vi.fn() })); vi.mock('../lib/profileApi', () => ({ @@ -56,15 +50,13 @@ const Probe = () => { describe('AuthContext DOM behavior', () => { beforeEach(() => { - getSessionMock.mockReset(); - signOutMock.mockReset(); + ensurePlatformSessionMock.mockReset(); tradingAuthState.user = { id: 'user-1', email: 'sarah@example.com', role: 'admin', name: 'Sarah Algo' }; tradingAuthState.isLoading = false; tradingAuthState.logout.mockReset(); fetchCurrentUserProfileMock.mockReset(); fetchTradeProfilesMock.mockReset(); - signOutMock.mockResolvedValue({ error: null }); fetchCurrentUserProfileMock.mockResolvedValue({ user_id: 'user-1', first_name: 'Sarah', @@ -77,8 +69,8 @@ describe('AuthContext DOM behavior', () => { }); it('loads session/profile, ensures default profile, and cleans up subscription', async () => { - getSessionMock.mockResolvedValue({ - data: { session: { user: { id: 'user-1' } } } + ensurePlatformSessionMock.mockResolvedValue({ + user: { id: 'user-1' } }); const dispatchSpy = vi.spyOn(window, 'dispatchEvent'); @@ -101,7 +93,7 @@ describe('AuthContext DOM behavior', () => { it('handles no initial session gracefully', async () => { tradingAuthState.user = null; - getSessionMock.mockResolvedValue({ data: { session: null } }); + ensurePlatformSessionMock.mockResolvedValue(null); render(); await waitFor(() => { @@ -112,7 +104,7 @@ describe('AuthContext DOM behavior', () => { }); it('handles auth state changes with no session', async () => { - getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } }); + ensurePlatformSessionMock.mockResolvedValue({ user: { id: 'u1' } }); const { rerender } = render(); await waitFor(() => { @@ -131,7 +123,7 @@ describe('AuthContext DOM behavior', () => { it('logs error when profile fetch fails', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); tradingAuthState.user = { id: 'u1', email: 'u1@example.com', role: 'member', name: 'U One' }; - getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } }); + ensurePlatformSessionMock.mockResolvedValue({ user: { id: 'u1' } }); fetchCurrentUserProfileMock.mockRejectedValue({ message: 'Profile Not Found' }); render(); @@ -145,7 +137,7 @@ describe('AuthContext DOM behavior', () => { it('handles unexpected errors in fetchProfile', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); tradingAuthState.user = { id: 'u1', email: 'u1@example.com', role: 'member', name: 'U One' }; - getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } }); + ensurePlatformSessionMock.mockResolvedValue({ user: { id: 'u1' } }); fetchCurrentUserProfileMock.mockImplementation(() => { throw new Error('Crashed'); }); render(); @@ -159,7 +151,7 @@ describe('AuthContext DOM behavior', () => { it('handles unexpected errors in ensureDefaultProfile', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { }); tradingAuthState.user = { id: 'u1', email: 'u1@example.com', role: 'member', name: 'U One' }; - getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } }); + ensurePlatformSessionMock.mockResolvedValue({ user: { id: 'u1' } }); fetchTradeProfilesMock.mockImplementation(() => { throw new Error('Limit Error'); }); render(); diff --git a/web/src/components/AuthContext.tsx b/web/src/components/AuthContext.tsx index f2b4f82..e11536e 100644 --- a/web/src/components/AuthContext.tsx +++ b/web/src/components/AuthContext.tsx @@ -1,8 +1,12 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; -import type { User, Session } from '@supabase/supabase-js'; -import { supabase } from '../lib/supabaseClient'; import { TradingAuthProvider, useTradingAuth } from '../lib/tradingAuth'; import { fetchCurrentUserProfile, fetchTradeProfiles } from '../lib/profileApi'; +import { + type PlatformSession, + type PlatformSessionUser, + clearPlatformSession, + ensurePlatformSession, +} from '../lib/authSession'; // Define the shape of our extended user profile export interface UserProfile { @@ -26,8 +30,8 @@ export interface UserProfile { } interface AuthContextType { - session: Session | null; - user: User | null; + session: PlatformSession | null; + user: PlatformSessionUser | null; profile: UserProfile | null; loading: boolean; signOut: () => Promise; @@ -36,16 +40,16 @@ interface AuthContextType { const AuthContext = createContext(undefined); -const buildFallbackProfile = (authUser: User | null): UserProfile | null => { +const buildFallbackProfile = (authUser: PlatformSessionUser | null): UserProfile | null => { if (!authUser?.id) return null; - const displayName = String((authUser as any)?.display_name || (authUser as any)?.user_metadata?.displayName || '').trim(); + const displayName = String(authUser?.display_name || (authUser as any)?.user_metadata?.displayName || '').trim(); const parts = displayName ? displayName.split(/\s+/) : []; return { user_id: authUser.id, first_name: parts[0] || '', last_name: parts.slice(1).join(' '), email: authUser.email || '', - role: String((authUser as any)?.role || (authUser as any)?.user_metadata?.role || 'member'), + role: String(authUser?.role || (authUser as any)?.user_metadata?.role || 'member'), trade_enable: true, }; }; @@ -85,8 +89,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { function AuthBridge({ children }: { children: React.ReactNode }) { const tradingAuth = useTradingAuth(); - const [session, setSession] = useState(null); - const [user, setUser] = useState(null); + const [session, setSession] = useState(null); + const [user, setUser] = useState(null); const [profile, setProfile] = useState(null); const [profileLoading, setProfileLoading] = useState(true); @@ -102,10 +106,10 @@ function AuthBridge({ children }: { children: React.ReactNode }) { return; } - const { data: { session: nextSession } } = await supabase.auth.getSession(); + const nextSession = await ensurePlatformSession(); if (!active) return; - const normalizedSession = (nextSession as Session | null) ?? null; - const normalizedUser = (normalizedSession?.user as User | null) ?? buildFallbackAuthUser(tradingAuth.user); + const normalizedSession = nextSession ?? null; + const normalizedUser = normalizedSession?.user ?? buildFallbackAuthUser(tradingAuth.user); setSession(normalizedSession); setUser(normalizedUser); await fetchProfile(tradingAuth.user.id, normalizedUser); @@ -118,7 +122,7 @@ function AuthBridge({ children }: { children: React.ReactNode }) { }; }, [tradingAuth.user?.id]); - const fetchProfile = async (_userId: string, authUserOverride?: User | null) => { + const fetchProfile = async (_userId: string, authUserOverride?: PlatformSessionUser | null) => { try { const currentProfile = await fetchCurrentUserProfile(); setProfile(currentProfile as UserProfile); @@ -145,8 +149,8 @@ function AuthBridge({ children }: { children: React.ReactNode }) { }; const signOut = async () => { - await supabase.auth.signOut(); tradingAuth.logout(); + clearPlatformSession(); setSession(null); setUser(null); setProfile(null); @@ -170,19 +174,18 @@ function AuthBridge({ children }: { children: React.ReactNode }) { return {children}; } -const buildFallbackAuthUser = (authUser: { id: string; email?: string; role?: string; name?: string; } | null): User | null => { +const buildFallbackAuthUser = (authUser: { id: string; email?: string; role?: string; name?: string; } | null): PlatformSessionUser | null => { if (!authUser?.id) return null; return { id: authUser.id, email: authUser.email || '', - aud: 'authenticated', - app_metadata: {}, + role: authUser.role || 'member', + display_name: authUser.name || authUser.email || '', user_metadata: { role: authUser.role || 'member', displayName: authUser.name || authUser.email || '', }, - created_at: new Date(0).toISOString(), - } as User; + }; }; export const useAuth = () => { diff --git a/web/src/components/ChatControl.dom.test.tsx b/web/src/components/ChatControl.dom.test.tsx index e48e4fa..9d8bb81 100644 --- a/web/src/components/ChatControl.dom.test.tsx +++ b/web/src/components/ChatControl.dom.test.tsx @@ -4,17 +4,13 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ChatControl } from './ChatControl'; -const { getSessionMock, writeTextMock } = vi.hoisted(() => ({ - getSessionMock: vi.fn(), +const { getPlatformAccessTokenMock, writeTextMock } = vi.hoisted(() => ({ + getPlatformAccessTokenMock: vi.fn(), writeTextMock: vi.fn() })); -vi.mock('../lib/supabaseClient', () => ({ - supabase: { - auth: { - getSession: getSessionMock - } - } +vi.mock('../lib/authSession', () => ({ + getPlatformAccessToken: getPlatformAccessTokenMock })); const profilesFixture = [ @@ -24,7 +20,7 @@ const profilesFixture = [ describe('ChatControl DOM flow', () => { beforeEach(() => { - getSessionMock.mockReset(); + getPlatformAccessTokenMock.mockReset(); writeTextMock.mockReset(); vi.stubGlobal('fetch', vi.fn()); @@ -35,7 +31,7 @@ describe('ChatControl DOM flow', () => { }); it('opens chat, shows quick actions, and handles unauthenticated send errors', async () => { - getSessionMock.mockResolvedValue({ data: { session: null } }); + getPlatformAccessTokenMock.mockRejectedValue(new Error('Not authenticated')); const onApplyProfile = vi.fn(async () => ({ success: true })); const user = userEvent.setup(); @@ -55,7 +51,7 @@ describe('ChatControl DOM flow', () => { }, 15000); it('sends prompt, edits profile draft, copies JSON, and applies profile', async () => { - getSessionMock.mockResolvedValue({ data: { session: { access_token: 'token-1' } } }); + getPlatformAccessTokenMock.mockResolvedValue('token-1'); const fetchMock = vi.mocked(fetch); fetchMock.mockResolvedValue({ ok: true, @@ -113,7 +109,7 @@ describe('ChatControl DOM flow', () => { }, 20000); it('marks generated profile action as cancelled', async () => { - getSessionMock.mockResolvedValue({ data: { session: { access_token: 'token-2' } } }); + getPlatformAccessTokenMock.mockResolvedValue('token-2'); vi.mocked(fetch).mockResolvedValue({ ok: true, json: async () => ({ diff --git a/web/src/components/ChatControl.tsx b/web/src/components/ChatControl.tsx index 3a0bc74..133dbe7 100644 --- a/web/src/components/ChatControl.tsx +++ b/web/src/components/ChatControl.tsx @@ -1,7 +1,7 @@ import { useState, useRef, useEffect, useMemo } from 'react'; import { createPortal } from 'react-dom'; -import { supabase } from '../lib/supabaseClient'; import { tradingRuntime } from '../lib/runtime'; +import { getPlatformAccessToken } from '../lib/authSession'; import { Send, X, Bot, User, Check, Loader2, @@ -199,11 +199,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => { try { const apiUrl = tradingRuntime.tradingApiUrl; - const { data: sessionData } = await supabase.auth.getSession(); - const accessToken = sessionData.session?.access_token; - if (!accessToken) { - throw new Error('Not authenticated'); - } + const accessToken = await getPlatformAccessToken(); const res = await fetch(`${apiUrl}/api/chat`, { method: 'POST', headers: { diff --git a/web/src/components/ComponentsSmoke.test.ts b/web/src/components/ComponentsSmoke.test.ts index bf0e62f..e9133fd 100644 --- a/web/src/components/ComponentsSmoke.test.ts +++ b/web/src/components/ComponentsSmoke.test.ts @@ -22,6 +22,7 @@ vi.mock('../lib/tradingAuth', () => ({ useTradingAuth: () => ({ login: vi.fn(async () => true), register: vi.fn(async () => true), + forgotPassword: vi.fn(async () => true), error: null, }) })); diff --git a/web/src/components/EntryForm.dom.test.tsx b/web/src/components/EntryForm.dom.test.tsx index ef3d9c9..9039f7d 100644 --- a/web/src/components/EntryForm.dom.test.tsx +++ b/web/src/components/EntryForm.dom.test.tsx @@ -5,12 +5,12 @@ import userEvent from '@testing-library/user-event'; import { EntryForm } from './EntryForm'; const { - getSessionMock, + getPlatformAccessTokenMock, createManualEntryMock, updateManualEntryMock, authMock } = vi.hoisted(() => ({ - getSessionMock: vi.fn(), + getPlatformAccessTokenMock: vi.fn(), createManualEntryMock: vi.fn(), updateManualEntryMock: vi.fn(), authMock: { user: { id: 'user-1' } as any } @@ -20,12 +20,8 @@ vi.mock('../components/AuthContext', () => ({ useAuth: () => authMock })); -vi.mock('../lib/supabaseClient', () => ({ - supabase: { - auth: { - getSession: getSessionMock - } - } +vi.mock('../lib/authSession', () => ({ + getPlatformAccessToken: getPlatformAccessTokenMock })); vi.mock('../lib/manualEntriesApi', () => ({ @@ -40,12 +36,12 @@ describe('EntryForm DOM flow', () => { vi.clearAllMocks(); createManualEntryMock.mockReset(); updateManualEntryMock.mockReset(); - getSessionMock.mockReset(); + getPlatformAccessTokenMock.mockReset(); authMock.user = { id: 'user-1' }; createManualEntryMock.mockResolvedValue({}); updateManualEntryMock.mockResolvedValue({}); - getSessionMock.mockResolvedValue({ data: { session: null } }); + getPlatformAccessTokenMock.mockRejectedValue(new Error('Not authenticated')); vi.stubGlobal('fetch', vi.fn()); vi.stubGlobal('confirm', vi.fn(() => true)); @@ -73,7 +69,7 @@ describe('EntryForm DOM flow', () => { it('alerts error when trade execution fails', async () => { const onSuccess = vi.fn(); const user = userEvent.setup(); - getSessionMock.mockResolvedValue({ data: { session: { access_token: 'valid-token' } } }); + getPlatformAccessTokenMock.mockResolvedValue('valid-token'); vi.mocked(fetch).mockResolvedValue({ ok: false, json: async () => ({ success: false, error: 'Insufficient funds' }) diff --git a/web/src/components/EntryForm.tsx b/web/src/components/EntryForm.tsx index 6821ea0..8010714 100644 --- a/web/src/components/EntryForm.tsx +++ b/web/src/components/EntryForm.tsx @@ -1,9 +1,9 @@ import { useState, useEffect } from 'react'; import type { FormEvent } from 'react'; -import { supabase } from '../lib/supabaseClient'; import { useAuth } from '../components/AuthContext'; import { tradingRuntime } from '../lib/runtime'; import { createManualEntry, updateManualEntry } from '../lib/manualEntriesApi'; +import { getPlatformAccessToken } from '../lib/authSession'; interface EntryFormProps { onSuccess: () => void; @@ -124,11 +124,7 @@ export function EntryForm({ onSuccess, initialData }: EntryFormProps) { if (!confirmTrade) return; const apiUrl = tradingRuntime.tradingApiUrl; - const { data: sessionData } = await supabase.auth.getSession(); - const accessToken = sessionData.session?.access_token; - if (!accessToken) { - throw new Error('Not authenticated'); - } + const accessToken = await getPlatformAccessToken(); const response = await fetch(`${apiUrl}/api/trade`, { method: 'POST', headers: { diff --git a/web/src/components/Login.dom.test.tsx b/web/src/components/Login.dom.test.tsx index fca2e73..66798f4 100644 --- a/web/src/components/Login.dom.test.tsx +++ b/web/src/components/Login.dom.test.tsx @@ -4,10 +4,10 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Login } from './Login'; -const { loginMock, registerMock, resetPasswordForEmailMock, tradingAuthState } = vi.hoisted(() => ({ +const { loginMock, registerMock, forgotPasswordMock, tradingAuthState } = vi.hoisted(() => ({ loginMock: vi.fn(), registerMock: vi.fn(), - resetPasswordForEmailMock: vi.fn(), + forgotPasswordMock: vi.fn(), tradingAuthState: { error: null as string | null } @@ -17,27 +17,20 @@ vi.mock('../lib/tradingAuth', () => ({ useTradingAuth: () => ({ login: loginMock, register: registerMock, + forgotPassword: forgotPasswordMock, error: tradingAuthState.error, }) })); -vi.mock('../lib/supabaseClient', () => ({ - supabase: { - auth: { - resetPasswordForEmail: resetPasswordForEmailMock - } - } -})); - describe('Login DOM flow', () => { beforeEach(() => { loginMock.mockReset(); registerMock.mockReset(); - resetPasswordForEmailMock.mockReset(); + forgotPasswordMock.mockReset(); tradingAuthState.error = null; loginMock.mockResolvedValue(true); registerMock.mockResolvedValue(true); - resetPasswordForEmailMock.mockResolvedValue({ error: null }); + forgotPasswordMock.mockResolvedValue(true); }); it('submits sign-in credentials and surfaces auth errors', async () => { @@ -90,12 +83,7 @@ describe('Login DOM flow', () => { await user.click(screen.getByRole('button', { name: 'Send Reset Link' })); await waitFor(() => { - expect(resetPasswordForEmailMock).toHaveBeenCalledWith( - 'recover@demo.com', - expect.objectContaining({ - redirectTo: expect.stringContaining('/reset-callback') - }) - ); + expect(forgotPasswordMock).toHaveBeenCalledWith('recover@demo.com'); expect(screen.getByText('Password reset link sent! Check your email.')).toBeInTheDocument(); }); diff --git a/web/src/components/Login.tsx b/web/src/components/Login.tsx index 36b07e0..d9640b8 100644 --- a/web/src/components/Login.tsx +++ b/web/src/components/Login.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import { supabase } from '../lib/supabaseClient'; import { useTradingAuth } from '../lib/tradingAuth'; export function Login() { @@ -20,10 +19,8 @@ export function Login() { try { if (isResetPassword) { - const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo: window.location.origin + '/reset-callback', - }); - if (error) throw error; + const ok = await tradingAuth.forgotPassword(email); + if (!ok) throw new Error(tradingAuth.error || 'Password reset failed'); setMessage('Password reset link sent! Check your email.'); } else if (isSignUp) { const ok = await tradingAuth.register(email, password, email.split('@')[0] || 'Trader'); diff --git a/web/src/components/ResetPassword.dom.test.tsx b/web/src/components/ResetPassword.dom.test.tsx index df54f65..c69e715 100644 --- a/web/src/components/ResetPassword.dom.test.tsx +++ b/web/src/components/ResetPassword.dom.test.tsx @@ -4,22 +4,18 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ResetPassword } from './ResetPassword'; -const { updateUserMock } = vi.hoisted(() => ({ - updateUserMock: vi.fn() +const { resetPlatformPasswordMock } = vi.hoisted(() => ({ + resetPlatformPasswordMock: vi.fn() })); -vi.mock('../lib/supabaseClient', () => ({ - supabase: { - auth: { - updateUser: updateUserMock - } - } +vi.mock('../lib/authSession', () => ({ + resetPlatformPassword: resetPlatformPasswordMock })); describe('ResetPassword DOM flow', () => { beforeEach(() => { - updateUserMock.mockReset(); - updateUserMock.mockResolvedValue({ error: null }); + resetPlatformPasswordMock.mockReset(); + resetPlatformPasswordMock.mockResolvedValue(undefined); window.history.pushState({}, '', '/reset-callback#type=recovery'); }); @@ -35,13 +31,13 @@ describe('ResetPassword DOM flow', () => { await user.click(screen.getByRole('button', { name: 'Update Password' })); await waitFor(() => { - expect(updateUserMock).toHaveBeenCalledWith({ password: 'MyNewPassword123!' }); + expect(resetPlatformPasswordMock).toHaveBeenCalledWith('MyNewPassword123!'); expect(screen.getByText('Password updated successfully! You can now login.')).toBeInTheDocument(); }); }, 15000); it('shows provider error when password update fails', async () => { - updateUserMock.mockResolvedValueOnce({ error: { message: 'Password is too weak' } }); + resetPlatformPasswordMock.mockRejectedValueOnce(new Error('Password is too weak')); const user = userEvent.setup(); render(); @@ -49,7 +45,7 @@ describe('ResetPassword DOM flow', () => { await user.click(screen.getByRole('button', { name: 'Update Password' })); await waitFor(() => { - expect(updateUserMock).toHaveBeenCalled(); + expect(resetPlatformPasswordMock).toHaveBeenCalled(); expect(screen.getByText('Password is too weak')).toBeInTheDocument(); }); diff --git a/web/src/components/ResetPassword.tsx b/web/src/components/ResetPassword.tsx index 80ba469..b01bea3 100644 --- a/web/src/components/ResetPassword.tsx +++ b/web/src/components/ResetPassword.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; -import { supabase } from '../lib/supabaseClient'; +import { useState, useEffect } from 'react'; +import { resetPlatformPassword } from '../lib/authSession'; export function ResetPassword() { const [password, setPassword] = useState(''); @@ -22,9 +22,8 @@ export function ResetPassword() { setError(null); setMessage(null); - try { - const { error } = await supabase.auth.updateUser({ password }); - if (error) throw error; + try { + await resetPlatformPassword(password); setMessage('Password updated successfully! You can now login.'); setTimeout(() => { if (typeof window !== 'undefined') { diff --git a/web/src/hooks/useWebSocket.dom.test.tsx b/web/src/hooks/useWebSocket.dom.test.tsx index c9be841..20714e0 100644 --- a/web/src/hooks/useWebSocket.dom.test.tsx +++ b/web/src/hooks/useWebSocket.dom.test.tsx @@ -3,17 +3,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { act, renderHook, waitFor } from '@testing-library/react'; import { useWebSocket } from './useWebSocket'; -const { getSessionMock, ioMock } = vi.hoisted(() => ({ - getSessionMock: vi.fn(), +const { getPlatformAccessTokenMock, ioMock } = vi.hoisted(() => ({ + getPlatformAccessTokenMock: vi.fn(), ioMock: vi.fn() })); -vi.mock('../lib/supabaseClient', () => ({ - supabase: { - auth: { - getSession: getSessionMock - } - } +vi.mock('../lib/authSession', () => ({ + getPlatformAccessToken: getPlatformAccessTokenMock })); vi.mock('socket.io-client', () => ({ @@ -26,7 +22,7 @@ describe('useWebSocket DOM/event behavior', () => { beforeEach(() => { Object.keys(handlers).forEach((key) => delete handlers[key]); - getSessionMock.mockReset(); + getPlatformAccessTokenMock.mockReset(); ioMock.mockReset(); socketStub = { @@ -40,12 +36,12 @@ describe('useWebSocket DOM/event behavior', () => { }); it('skips socket connection when there is no auth session token', async () => { - getSessionMock.mockResolvedValue({ data: { session: null } }); + getPlatformAccessTokenMock.mockRejectedValue(new Error('Not authenticated')); const { result } = renderHook(() => useWebSocket('http://localhost:5000')); await waitFor(() => { - expect(getSessionMock).toHaveBeenCalledTimes(1); + expect(getPlatformAccessTokenMock).toHaveBeenCalledTimes(1); }); expect(ioMock).not.toHaveBeenCalled(); @@ -54,7 +50,7 @@ describe('useWebSocket DOM/event behavior', () => { }); it('connects with token and applies websocket event updates', async () => { - getSessionMock.mockResolvedValue({ data: { session: { access_token: 'token-abc' } } }); + getPlatformAccessTokenMock.mockResolvedValue('token-abc'); const { result, unmount } = renderHook(() => useWebSocket('http://localhost:5000')); diff --git a/web/src/hooks/useWebSocket.ts b/web/src/hooks/useWebSocket.ts index 7ffc2b3..fa0ea19 100644 --- a/web/src/hooks/useWebSocket.ts +++ b/web/src/hooks/useWebSocket.ts @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { io, Socket } from 'socket.io-client'; import { buildTradingSocketOptions } from '../../../shared/realtime.js'; -import { supabase } from '../lib/supabaseClient'; +import { getPlatformAccessToken } from '../lib/authSession'; export interface TradingControlSnapshot { mode: 'RUNNING' | 'PAUSED'; @@ -276,8 +276,7 @@ export const useWebSocket = (url: string) => { const connectSocket = async () => { console.log('🔌 Attempting to connect to:', url); - const { data } = await supabase.auth.getSession(); - const token = data.session?.access_token; + const token = await getPlatformAccessToken().catch(() => null); if (!token) { console.warn('Socket connection skipped: missing authenticated session token'); diff --git a/web/src/lib/authSession.ts b/web/src/lib/authSession.ts index 7a2b651..bc848e7 100644 --- a/web/src/lib/authSession.ts +++ b/web/src/lib/authSession.ts @@ -2,6 +2,7 @@ const AUTH_STORAGE_PREFIX = 'invttrdg_web'; const ACCESS_TOKEN_KEY = `${AUTH_STORAGE_PREFIX}_access_token`; const REFRESH_TOKEN_KEY = `${AUTH_STORAGE_PREFIX}_refresh_token`; const USER_KEY = `${AUTH_STORAGE_PREFIX}_auth_user`; +const AUTH_CHANGE_EVENT = 'trading-platform-auth-change'; export interface PlatformSessionUser { id: string; @@ -18,6 +19,16 @@ export interface PlatformSession { user: PlatformSessionUser; } +class PlatformAuthError extends Error { + status?: number; + + constructor(message: string, status?: number) { + super(message); + this.name = 'PlatformAuthError'; + this.status = status; + } +} + function parseJson(value: string | null): T | null { if (!value) return null; try { @@ -42,7 +53,166 @@ export function getStoredPlatformSession(): PlatformSession | null { }; } -export function getPlatformAccessToken(): string { +function savePlatformSession(session: PlatformSession): void { + if (typeof window === 'undefined') return; + window.localStorage.setItem(ACCESS_TOKEN_KEY, session.access_token); + window.localStorage.setItem(REFRESH_TOKEN_KEY, session.refresh_token); + window.localStorage.setItem(USER_KEY, JSON.stringify(session.user)); +} + +export function clearPlatformSession(): void { + if (typeof window === 'undefined') return; + window.localStorage.removeItem(ACCESS_TOKEN_KEY); + window.localStorage.removeItem(REFRESH_TOKEN_KEY); + window.localStorage.removeItem(USER_KEY); +} + +export function emitPlatformAuthChange(event: string, session: PlatformSession | null): void { + if (typeof window === 'undefined') return; + window.dispatchEvent(new CustomEvent(AUTH_CHANGE_EVENT, { detail: { event, session } })); +} + +export function subscribePlatformAuthChange( + callback: (event: string, session: PlatformSession | null) => void +): () => void { + if (typeof window === 'undefined') { + return () => {}; + } + + const handler = (rawEvent: Event) => { + const event = rawEvent as CustomEvent<{ event?: string; session?: PlatformSession | null }>; + callback(String(event.detail?.event || 'UNKNOWN'), event.detail?.session ?? null); + }; + + window.addEventListener(AUTH_CHANGE_EVENT, handler as EventListener); + return () => { + window.removeEventListener(AUTH_CHANGE_EVENT, handler as EventListener); + }; +} + +function decodeJwtPayload(token: string): Record | null { + try { + const [, payload] = token.split('.'); + if (!payload) return null; + return JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/'))); + } catch { + return null; + } +} + +function isAccessTokenFresh(token: string): boolean { + const claims = decodeJwtPayload(token); + const exp = Number(claims?.exp || 0); + if (!exp) return false; + return exp > Math.floor(Date.now() / 1000) + 60; +} + +function normalizeUser(input: any): PlatformSessionUser { + return { + id: String(input?.id || input?.sub || '').trim(), + email: typeof input?.email === 'string' ? input.email : undefined, + role: typeof input?.role === 'string' ? input.role : undefined, + plan: typeof input?.plan === 'string' ? input.plan : undefined, + display_name: typeof input?.displayName === 'string' ? input.displayName : undefined, + user_metadata: { + role: input?.role, + plan: input?.plan, + displayName: input?.displayName, + }, + }; +} + +async function platformRequest( + path: string, + options?: { + method?: string; + accessToken?: string; + body?: Record; + } +): Promise { + const runtimeModule = await import('./runtime'); + const response = await fetch(`${runtimeModule.tradingRuntime.platformApiUrl}${path}`, { + method: options?.method || 'GET', + headers: { + 'Content-Type': 'application/json', + 'x-product-id': runtimeModule.tradingRuntime.productId, + ...(options?.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {}), + }, + body: options?.body ? JSON.stringify(options.body) : undefined, + }); + + const payload = await response.json().catch(() => ({})); + if (!response.ok) { + throw new PlatformAuthError( + String((payload as { message?: string; error?: string }).message || (payload as { error?: string }).error || `HTTP ${response.status}`), + response.status + ); + } + + return payload as T; +} + +async function getPlatformUser(accessToken: string): Promise { + const me = await platformRequest('/auth/me', { accessToken }); + return normalizeUser(me); +} + +async function refreshPlatformSession(refreshToken: string): Promise { + const refreshed = await platformRequest<{ accessToken: string; refreshToken: string }>('/auth/refresh', { + method: 'POST', + body: { refreshToken }, + }); + const user = await getPlatformUser(refreshed.accessToken); + const nextSession: PlatformSession = { + access_token: refreshed.accessToken, + refresh_token: refreshed.refreshToken, + user, + }; + savePlatformSession(nextSession); + return nextSession; +} + +export async function ensurePlatformSession(): Promise { + const stored = getStoredPlatformSession(); + if (!stored) { + return null; + } + + if (isAccessTokenFresh(stored.access_token) && stored.user?.id) { + return stored; + } + + try { + const user = await getPlatformUser(stored.access_token); + const nextSession = { ...stored, user }; + savePlatformSession(nextSession); + return nextSession; + } catch (error) { + if ((error as PlatformAuthError)?.status === 401 || (error as PlatformAuthError)?.status === 403) { + try { + const refreshed = await refreshPlatformSession(stored.refresh_token); + emitPlatformAuthChange('TOKEN_REFRESHED', refreshed); + return refreshed; + } catch { + clearPlatformSession(); + emitPlatformAuthChange('SIGNED_OUT', null); + return null; + } + } + throw error; + } +} + +export async function getPlatformAccessToken(): Promise { + const session = await ensurePlatformSession(); + const accessToken = session?.access_token; + if (!accessToken) { + throw new Error('Not authenticated'); + } + return accessToken; +} + +export function getPlatformAccessTokenSync(): string { const session = getStoredPlatformSession(); const accessToken = session?.access_token; if (!accessToken) { @@ -50,3 +220,27 @@ export function getPlatformAccessToken(): string { } return accessToken; } + +function getPasswordResetToken(): string | null { + if (typeof window === 'undefined') return null; + const url = new URL(window.location.href); + const directToken = url.searchParams.get('token'); + if (directToken) return directToken; + + const hashParams = new URLSearchParams(url.hash.replace(/^#/, '')); + return hashParams.get('token'); +} + +export async function resetPlatformPassword(newPassword: string): Promise { + const token = getPasswordResetToken(); + if (!token) { + throw new Error('Missing password reset token'); + } + await platformRequest('/auth/reset-password', { + method: 'POST', + body: { + token, + newPassword, + }, + }); +} diff --git a/web/src/lib/dynamicConfigApi.ts b/web/src/lib/dynamicConfigApi.ts index 6b2c5be..aa338db 100644 --- a/web/src/lib/dynamicConfigApi.ts +++ b/web/src/lib/dynamicConfigApi.ts @@ -1,4 +1,4 @@ -import { supabase } from './supabaseClient'; +import { getPlatformAccessToken } from './authSession'; import { tradingRuntime } from './runtime'; export interface DynamicConfigItem { @@ -7,17 +7,8 @@ export interface DynamicConfigItem { description: string; } -async function getAccessToken(): Promise { - const { data: sessionData } = await supabase.auth.getSession(); - const accessToken = sessionData.session?.access_token; - if (!accessToken) { - throw new Error('Not authenticated'); - } - return accessToken; -} - export async function fetchDynamicConfigItems(): Promise { - const accessToken = await getAccessToken(); + const accessToken = await getPlatformAccessToken(); const response = await fetch(`${tradingRuntime.tradingApiUrl}/api/admin/config/dynamic`, { headers: { Authorization: `Bearer ${accessToken}`, @@ -33,7 +24,7 @@ export async function fetchDynamicConfigItems(): Promise { } export async function upsertDynamicConfigItems(items: DynamicConfigItem[]): Promise { - const accessToken = await getAccessToken(); + const accessToken = await getPlatformAccessToken(); const response = await fetch(`${tradingRuntime.tradingApiUrl}/api/admin/config/dynamic`, { method: 'PUT', headers: { diff --git a/web/src/lib/supabaseClient.ts b/web/src/lib/supabaseClient.ts index e081f77..c044ad9 100644 --- a/web/src/lib/supabaseClient.ts +++ b/web/src/lib/supabaseClient.ts @@ -1,37 +1,7 @@ import { createClient } from '@supabase/supabase-js'; import { getWebSupabaseConfig } from '../../../shared/supabase-config.js'; -import { getRuntimeEnvironment } from '../../../shared/runtime.js'; const supabaseConfig = getWebSupabaseConfig(); -const runtime = getRuntimeEnvironment('web'); -const AUTH_STORAGE_PREFIX = 'invttrdg_web'; -const ACCESS_TOKEN_KEY = `${AUTH_STORAGE_PREFIX}_access_token`; -const REFRESH_TOKEN_KEY = `${AUTH_STORAGE_PREFIX}_refresh_token`; -const USER_KEY = `${AUTH_STORAGE_PREFIX}_auth_user`; -const authListeners = new Set<(event: string, session: any) => void>(); - -type PlatformSession = { - access_token: string; - refresh_token: string; - user: { - id: string; - email?: string; - role?: string; - plan?: string; - display_name?: string; - user_metadata?: Record; - }; -}; - -class PlatformAuthError extends Error { - status?: number; - - constructor(message: string, status?: number) { - super(message); - this.name = 'PlatformAuthError'; - this.status = status; - } -} if (!supabaseConfig.isConfigured) { console.warn('Missing Supabase environment variables for legacy data client fallback'); @@ -41,287 +11,6 @@ const dataClient = supabaseConfig.isConfigured ? createClient(supabaseConfig.url, supabaseConfig.anonKey) : null; -function parseJson(value: string | null): T | null { - if (!value) return null; - try { - return JSON.parse(value) as T; - } catch { - return null; - } -} - -function getStoredSession(): PlatformSession | null { - if (typeof window === 'undefined') return null; - const accessToken = window.localStorage.getItem(ACCESS_TOKEN_KEY); - const refreshToken = window.localStorage.getItem(REFRESH_TOKEN_KEY); - const user = parseJson(window.localStorage.getItem(USER_KEY)); - if (!accessToken || !refreshToken || !user?.id) { - return null; - } - return { - access_token: accessToken, - refresh_token: refreshToken, - user, - }; -} - -function saveSession(session: PlatformSession): void { - window.localStorage.setItem(ACCESS_TOKEN_KEY, session.access_token); - window.localStorage.setItem(REFRESH_TOKEN_KEY, session.refresh_token); - window.localStorage.setItem(USER_KEY, JSON.stringify(session.user)); -} - -function clearSession(): void { - window.localStorage.removeItem(ACCESS_TOKEN_KEY); - window.localStorage.removeItem(REFRESH_TOKEN_KEY); - window.localStorage.removeItem(USER_KEY); -} - -function emitAuthChange(event: string, session: PlatformSession | null): void { - for (const listener of authListeners) { - listener(event, session); - } -} - -function decodeJwtPayload(token: string): Record | null { - try { - const [, payload] = token.split('.'); - if (!payload) return null; - return JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/'))); - } catch { - return null; - } -} - -function isAccessTokenFresh(token: string): boolean { - const claims = decodeJwtPayload(token); - const exp = Number(claims?.exp || 0); - if (!exp) return false; - return exp > Math.floor(Date.now() / 1000) + 60; -} - -function normalizeUser(input: any): PlatformSession['user'] { - return { - id: String(input?.id || input?.sub || '').trim(), - email: typeof input?.email === 'string' ? input.email : undefined, - role: typeof input?.role === 'string' ? input.role : undefined, - plan: typeof input?.plan === 'string' ? input.plan : undefined, - display_name: typeof input?.displayName === 'string' ? input.displayName : undefined, - user_metadata: { - role: input?.role, - plan: input?.plan, - displayName: input?.displayName, - }, - }; -} - -async function platformRequest( - path: string, - options?: { - method?: string; - accessToken?: string; - body?: Record; - } -): Promise { - const response = await fetch(`${runtime.platformApiUrl}${path}`, { - method: options?.method || 'GET', - headers: { - 'Content-Type': 'application/json', - 'x-product-id': runtime.productId, - ...(options?.accessToken ? { Authorization: `Bearer ${options.accessToken}` } : {}), - }, - body: options?.body ? JSON.stringify(options.body) : undefined, - }); - - const payload = await response.json().catch(() => ({})); - if (!response.ok) { - throw new PlatformAuthError( - String((payload as { message?: string; error?: string }).message || (payload as { error?: string }).error || `HTTP ${response.status}`), - response.status - ); - } - - return payload as T; -} - -async function getPlatformUser(accessToken: string): Promise { - const me = await platformRequest('/auth/me', { accessToken }); - return normalizeUser(me); -} - -async function refreshPlatformSession(refreshToken: string): Promise { - const refreshed = await platformRequest<{ accessToken: string; refreshToken: string }>('/auth/refresh', { - method: 'POST', - body: { refreshToken }, - }); - const user = await getPlatformUser(refreshed.accessToken); - const nextSession: PlatformSession = { - access_token: refreshed.accessToken, - refresh_token: refreshed.refreshToken, - user, - }; - saveSession(nextSession); - return nextSession; -} - -async function ensurePlatformSession(): Promise { - const stored = getStoredSession(); - if (!stored) { - return null; - } - - if (isAccessTokenFresh(stored.access_token) && stored.user?.id) { - return stored; - } - - try { - const user = await getPlatformUser(stored.access_token); - const nextSession = { ...stored, user }; - saveSession(nextSession); - return nextSession; - } catch (error) { - if ((error as PlatformAuthError)?.status === 401 || (error as PlatformAuthError)?.status === 403) { - try { - const refreshed = await refreshPlatformSession(stored.refresh_token); - emitAuthChange('TOKEN_REFRESHED', refreshed); - return refreshed; - } catch { - clearSession(); - emitAuthChange('SIGNED_OUT', null); - return null; - } - } - throw error; - } -} - -function getPasswordResetToken(): string | null { - if (typeof window === 'undefined') return null; - const url = new URL(window.location.href); - const directToken = url.searchParams.get('token'); - if (directToken) return directToken; - - const hashParams = new URLSearchParams(url.hash.replace(/^#/, '')); - return hashParams.get('token'); -} - -const auth = { - async getSession() { - return { data: { session: await ensurePlatformSession() } }; - }, - - onAuthStateChange(callback: (event: string, session: PlatformSession | null) => void) { - authListeners.add(callback); - return { - data: { - subscription: { - unsubscribe() { - authListeners.delete(callback); - } - } - } - }; - }, - - async signInWithPassword({ email, password }: { email: string; password: string; }) { - try { - const response = await platformRequest<{ - accessToken: string; - refreshToken: string; - user: unknown; - }>('/auth/login', { - method: 'POST', - body: { - email, - password, - productId: runtime.productId, - }, - }); - - const session: PlatformSession = { - access_token: response.accessToken, - refresh_token: response.refreshToken, - user: normalizeUser(response.user), - }; - saveSession(session); - emitAuthChange('SIGNED_IN', session); - return { data: { session }, error: null }; - } catch (error) { - return { data: { session: null }, error }; - } - }, - - async signUp({ email, password }: { email: string; password: string; }) { - try { - const response = await platformRequest<{ - accessToken: string; - refreshToken: string; - user: unknown; - }>('/auth/register', { - method: 'POST', - body: { - email, - password, - displayName: email.split('@')[0], - productId: runtime.productId, - }, - }); - - const session: PlatformSession = { - access_token: response.accessToken, - refresh_token: response.refreshToken, - user: normalizeUser(response.user), - }; - saveSession(session); - emitAuthChange('SIGNED_IN', session); - return { data: { session }, error: null }; - } catch (error) { - return { data: { session: null }, error }; - } - }, - - async signOut() { - clearSession(); - emitAuthChange('SIGNED_OUT', null); - return { error: null }; - }, - - async resetPasswordForEmail(email: string, _options?: { redirectTo?: string; }) { - try { - void _options; - await platformRequest('/auth/forgot-password', { - method: 'POST', - body: { - email, - productId: runtime.productId, - }, - }); - return { data: {}, error: null }; - } catch (error) { - return { data: {}, error }; - } - }, - - async updateUser({ password }: { password: string; }) { - try { - const token = getPasswordResetToken(); - if (!token) { - throw new PlatformAuthError('Missing password reset token'); - } - await platformRequest('/auth/reset-password', { - method: 'POST', - body: { - token, - newPassword: password, - }, - }); - return { data: {}, error: null }; - } catch (error) { - return { data: {}, error }; - } - }, -}; - export const supabase = { from: (...args: any[]) => { if (!dataClient) { @@ -329,5 +18,4 @@ export const supabase = { } return (dataClient.from as any)(...args); }, - auth, }; diff --git a/web/src/tabs/AdminTab.tsx b/web/src/tabs/AdminTab.tsx index 437c53e..d289652 100644 --- a/web/src/tabs/AdminTab.tsx +++ b/web/src/tabs/AdminTab.tsx @@ -10,12 +10,12 @@ import { ChevronRight, Pause, Play, AlertTriangle, Database, RefreshCcw, Heart, Info, XCircle } from 'lucide-react'; -import { supabase } from '../lib/supabaseClient'; import type { BotState } from '../hooks/useWebSocket'; import { useWebSocket } from '../hooks/useWebSocket'; import { useAuth } from '../components/AuthContext'; import { tradingRuntime } from '../lib/runtime'; import { fetchDynamicConfigItems, upsertDynamicConfigItems } from '../lib/dynamicConfigApi'; +import { getPlatformAccessToken } from '../lib/authSession'; interface AdminTabProps { botState: BotState; @@ -153,11 +153,7 @@ export const AdminTab = ({ botState }: AdminTabProps) => { setControlError(null); try { const apiUrl = tradingRuntime.tradingApiUrl; - const { data: sessionData } = await supabase.auth.getSession(); - const accessToken = sessionData.session?.access_token; - if (!accessToken) { - throw new Error('Not authenticated'); - } + const accessToken = await getPlatformAccessToken(); const res = await fetch(`${apiUrl}/internal/trading/pause`, { method: 'POST', headers: { @@ -182,11 +178,7 @@ export const AdminTab = ({ botState }: AdminTabProps) => { setControlError(null); try { const apiUrl = tradingRuntime.tradingApiUrl; - const { data: sessionData } = await supabase.auth.getSession(); - const accessToken = sessionData.session?.access_token; - if (!accessToken) { - throw new Error('Not authenticated'); - } + const accessToken = await getPlatformAccessToken(); const res = await fetch(`${apiUrl}/internal/trading/resume`, { method: 'POST', headers: { @@ -211,11 +203,7 @@ export const AdminTab = ({ botState }: AdminTabProps) => { setControlError(null); try { const apiUrl = tradingRuntime.tradingApiUrl; - const { data: sessionData } = await supabase.auth.getSession(); - const accessToken = sessionData.session?.access_token; - if (!accessToken) { - throw new Error('Not authenticated'); - } + const accessToken = await getPlatformAccessToken(); const res = await fetch(`${apiUrl}/api/events`, { method: 'DELETE', headers: { @@ -238,8 +226,7 @@ export const AdminTab = ({ botState }: AdminTabProps) => { const fetchConfig = async () => { try { const apiUrl = tradingRuntime.tradingApiUrl; - const { data: sessionData } = await supabase.auth.getSession(); - const accessToken = sessionData.session?.access_token; + const accessToken = await getPlatformAccessToken().catch(() => null); if (!accessToken) return; const res = await fetch(`${apiUrl}/api/config`, { headers: { diff --git a/web/src/tabs/ReconciliationAuditPanel.tsx b/web/src/tabs/ReconciliationAuditPanel.tsx index a8873ee..577bce2 100644 --- a/web/src/tabs/ReconciliationAuditPanel.tsx +++ b/web/src/tabs/ReconciliationAuditPanel.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { AlertTriangle, Clock3, RefreshCcw, Search, ShieldCheck, Undo2 } from 'lucide-react'; -import { supabase } from '../lib/supabaseClient'; import { tradingRuntime } from '../lib/runtime'; +import { getPlatformAccessToken } from '../lib/authSession'; interface ReconciliationBackfillAuditRow { id: number; @@ -158,11 +158,7 @@ export const ReconciliationAuditPanel = () => { setIsLoading(true); setError(null); try { - const { data: sessionData } = await supabase.auth.getSession(); - const accessToken = sessionData.session?.access_token; - if (!accessToken) { - throw new Error('Not authenticated'); - } + const accessToken = await getPlatformAccessToken(); const apiUrl = tradingRuntime.tradingApiUrl; const auditParams = buildQueryParams(filters, PAGE_LIMIT, offset); @@ -262,9 +258,7 @@ export const ReconciliationAuditPanel = () => { setIsReverting(batchId); setError(null); try { - const { data: sessionData } = await supabase.auth.getSession(); - const accessToken = sessionData.session?.access_token; - if (!accessToken) throw new Error('Not authenticated'); + const accessToken = await getPlatformAccessToken(); const apiUrl = tradingRuntime.tradingApiUrl;