refactor: move web auth onto platform session helpers
This commit is contained in:
parent
541c617717
commit
6c39b9b185
@ -1,13 +1,9 @@
|
|||||||
import { supabase } from '../lib/supabaseClient';
|
import { getPlatformAccessToken } from '../lib/authSession';
|
||||||
import type { BacktestRequestPayload, BacktestResult } from './types';
|
import type { BacktestRequestPayload, BacktestResult } from './types';
|
||||||
import { tradingRuntime } from '../lib/runtime';
|
import { tradingRuntime } from '../lib/runtime';
|
||||||
|
|
||||||
export const runBacktestApi = async (payload: BacktestRequestPayload): Promise<BacktestResult> => {
|
export const runBacktestApi = async (payload: BacktestRequestPayload): Promise<BacktestResult> => {
|
||||||
const { data: sessionData } = await supabase.auth.getSession();
|
const accessToken = await getPlatformAccessToken();
|
||||||
const accessToken = sessionData.session?.access_token;
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error('Not authenticated');
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiUrl = tradingRuntime.tradingApiUrl;
|
const apiUrl = tradingRuntime.tradingApiUrl;
|
||||||
const response = await fetch(`${apiUrl}/api/backtest/run`, {
|
const response = await fetch(`${apiUrl}/api/backtest/run`, {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { supabase } from '../lib/supabaseClient';
|
import { getPlatformAccessToken } from '../lib/authSession';
|
||||||
import { tradingRuntime } from '../lib/runtime';
|
import { tradingRuntime } from '../lib/runtime';
|
||||||
|
|
||||||
export interface BacktestRuntimeFlags {
|
export interface BacktestRuntimeFlags {
|
||||||
@ -44,8 +44,7 @@ export const loadBacktestRuntimeFlags = async (): Promise<BacktestRuntimeFlags>
|
|||||||
return runtimeFlagsCache;
|
return runtimeFlagsCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionData = await supabase.auth.getSession();
|
const accessToken = await getPlatformAccessToken().catch(() => null);
|
||||||
const accessToken = sessionData?.data?.session?.access_token;
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
const fallback = { enableBacktest: false, customerEnabled: false };
|
const fallback = { enableBacktest: false, customerEnabled: false };
|
||||||
runtimeFlagsCache = fallback;
|
runtimeFlagsCache = fallback;
|
||||||
|
|||||||
@ -5,14 +5,12 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|||||||
import { AuthProvider, useAuth } from './AuthContext';
|
import { AuthProvider, useAuth } from './AuthContext';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getSessionMock,
|
ensurePlatformSessionMock,
|
||||||
signOutMock,
|
|
||||||
tradingAuthState,
|
tradingAuthState,
|
||||||
fetchCurrentUserProfileMock,
|
fetchCurrentUserProfileMock,
|
||||||
fetchTradeProfilesMock
|
fetchTradeProfilesMock
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
getSessionMock: vi.fn(),
|
ensurePlatformSessionMock: vi.fn(),
|
||||||
signOutMock: vi.fn(),
|
|
||||||
tradingAuthState: {
|
tradingAuthState: {
|
||||||
user: { id: 'user-1', email: 'sarah@example.com', role: 'admin', name: 'Sarah Algo' } as any,
|
user: { id: 'user-1', email: 'sarah@example.com', role: 'admin', name: 'Sarah Algo' } as any,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -27,13 +25,9 @@ vi.mock('../lib/tradingAuth', () => ({
|
|||||||
useTradingAuth: () => tradingAuthState
|
useTradingAuth: () => tradingAuthState
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../lib/supabaseClient', () => ({
|
vi.mock('../lib/authSession', () => ({
|
||||||
supabase: {
|
ensurePlatformSession: ensurePlatformSessionMock,
|
||||||
auth: {
|
clearPlatformSession: vi.fn()
|
||||||
getSession: getSessionMock,
|
|
||||||
signOut: signOutMock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../lib/profileApi', () => ({
|
vi.mock('../lib/profileApi', () => ({
|
||||||
@ -56,15 +50,13 @@ const Probe = () => {
|
|||||||
|
|
||||||
describe('AuthContext DOM behavior', () => {
|
describe('AuthContext DOM behavior', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getSessionMock.mockReset();
|
ensurePlatformSessionMock.mockReset();
|
||||||
signOutMock.mockReset();
|
|
||||||
tradingAuthState.user = { id: 'user-1', email: 'sarah@example.com', role: 'admin', name: 'Sarah Algo' };
|
tradingAuthState.user = { id: 'user-1', email: 'sarah@example.com', role: 'admin', name: 'Sarah Algo' };
|
||||||
tradingAuthState.isLoading = false;
|
tradingAuthState.isLoading = false;
|
||||||
tradingAuthState.logout.mockReset();
|
tradingAuthState.logout.mockReset();
|
||||||
fetchCurrentUserProfileMock.mockReset();
|
fetchCurrentUserProfileMock.mockReset();
|
||||||
fetchTradeProfilesMock.mockReset();
|
fetchTradeProfilesMock.mockReset();
|
||||||
|
|
||||||
signOutMock.mockResolvedValue({ error: null });
|
|
||||||
fetchCurrentUserProfileMock.mockResolvedValue({
|
fetchCurrentUserProfileMock.mockResolvedValue({
|
||||||
user_id: 'user-1',
|
user_id: 'user-1',
|
||||||
first_name: 'Sarah',
|
first_name: 'Sarah',
|
||||||
@ -77,8 +69,8 @@ describe('AuthContext DOM behavior', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('loads session/profile, ensures default profile, and cleans up subscription', async () => {
|
it('loads session/profile, ensures default profile, and cleans up subscription', async () => {
|
||||||
getSessionMock.mockResolvedValue({
|
ensurePlatformSessionMock.mockResolvedValue({
|
||||||
data: { session: { user: { id: 'user-1' } } }
|
user: { id: 'user-1' }
|
||||||
});
|
});
|
||||||
const dispatchSpy = vi.spyOn(window, 'dispatchEvent');
|
const dispatchSpy = vi.spyOn(window, 'dispatchEvent');
|
||||||
|
|
||||||
@ -101,7 +93,7 @@ describe('AuthContext DOM behavior', () => {
|
|||||||
|
|
||||||
it('handles no initial session gracefully', async () => {
|
it('handles no initial session gracefully', async () => {
|
||||||
tradingAuthState.user = null;
|
tradingAuthState.user = null;
|
||||||
getSessionMock.mockResolvedValue({ data: { session: null } });
|
ensurePlatformSessionMock.mockResolvedValue(null);
|
||||||
render(<AuthProvider><Probe /></AuthProvider>);
|
render(<AuthProvider><Probe /></AuthProvider>);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@ -112,7 +104,7 @@ describe('AuthContext DOM behavior', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('handles auth state changes with no session', async () => {
|
it('handles auth state changes with no session', async () => {
|
||||||
getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
|
ensurePlatformSessionMock.mockResolvedValue({ user: { id: 'u1' } });
|
||||||
const { rerender } = render(<AuthProvider><Probe /></AuthProvider>);
|
const { rerender } = render(<AuthProvider><Probe /></AuthProvider>);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
@ -131,7 +123,7 @@ describe('AuthContext DOM behavior', () => {
|
|||||||
it('logs error when profile fetch fails', async () => {
|
it('logs error when profile fetch fails', async () => {
|
||||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
||||||
tradingAuthState.user = { id: 'u1', email: 'u1@example.com', role: 'member', name: 'U One' };
|
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' });
|
fetchCurrentUserProfileMock.mockRejectedValue({ message: 'Profile Not Found' });
|
||||||
|
|
||||||
render(<AuthProvider><Probe /></AuthProvider>);
|
render(<AuthProvider><Probe /></AuthProvider>);
|
||||||
@ -145,7 +137,7 @@ describe('AuthContext DOM behavior', () => {
|
|||||||
it('handles unexpected errors in fetchProfile', async () => {
|
it('handles unexpected errors in fetchProfile', async () => {
|
||||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
||||||
tradingAuthState.user = { id: 'u1', email: 'u1@example.com', role: 'member', name: 'U One' };
|
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'); });
|
fetchCurrentUserProfileMock.mockImplementation(() => { throw new Error('Crashed'); });
|
||||||
|
|
||||||
render(<AuthProvider><Probe /></AuthProvider>);
|
render(<AuthProvider><Probe /></AuthProvider>);
|
||||||
@ -159,7 +151,7 @@ describe('AuthContext DOM behavior', () => {
|
|||||||
it('handles unexpected errors in ensureDefaultProfile', async () => {
|
it('handles unexpected errors in ensureDefaultProfile', async () => {
|
||||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
||||||
tradingAuthState.user = { id: 'u1', email: 'u1@example.com', role: 'member', name: 'U One' };
|
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'); });
|
fetchTradeProfilesMock.mockImplementation(() => { throw new Error('Limit Error'); });
|
||||||
|
|
||||||
render(<AuthProvider><Probe /></AuthProvider>);
|
render(<AuthProvider><Probe /></AuthProvider>);
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
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 { TradingAuthProvider, useTradingAuth } from '../lib/tradingAuth';
|
||||||
import { fetchCurrentUserProfile, fetchTradeProfiles } from '../lib/profileApi';
|
import { fetchCurrentUserProfile, fetchTradeProfiles } from '../lib/profileApi';
|
||||||
|
import {
|
||||||
|
type PlatformSession,
|
||||||
|
type PlatformSessionUser,
|
||||||
|
clearPlatformSession,
|
||||||
|
ensurePlatformSession,
|
||||||
|
} from '../lib/authSession';
|
||||||
|
|
||||||
// Define the shape of our extended user profile
|
// Define the shape of our extended user profile
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
@ -26,8 +30,8 @@ export interface UserProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
session: Session | null;
|
session: PlatformSession | null;
|
||||||
user: User | null;
|
user: PlatformSessionUser | null;
|
||||||
profile: UserProfile | null;
|
profile: UserProfile | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
signOut: () => Promise<void>;
|
signOut: () => Promise<void>;
|
||||||
@ -36,16 +40,16 @@ interface AuthContextType {
|
|||||||
|
|
||||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
const buildFallbackProfile = (authUser: User | null): UserProfile | null => {
|
const buildFallbackProfile = (authUser: PlatformSessionUser | null): UserProfile | null => {
|
||||||
if (!authUser?.id) return 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+/) : [];
|
const parts = displayName ? displayName.split(/\s+/) : [];
|
||||||
return {
|
return {
|
||||||
user_id: authUser.id,
|
user_id: authUser.id,
|
||||||
first_name: parts[0] || '',
|
first_name: parts[0] || '',
|
||||||
last_name: parts.slice(1).join(' '),
|
last_name: parts.slice(1).join(' '),
|
||||||
email: authUser.email || '',
|
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,
|
trade_enable: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -85,8 +89,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
function AuthBridge({ children }: { children: React.ReactNode }) {
|
function AuthBridge({ children }: { children: React.ReactNode }) {
|
||||||
const tradingAuth = useTradingAuth();
|
const tradingAuth = useTradingAuth();
|
||||||
const [session, setSession] = useState<Session | null>(null);
|
const [session, setSession] = useState<PlatformSession | null>(null);
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<PlatformSessionUser | null>(null);
|
||||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||||
const [profileLoading, setProfileLoading] = useState(true);
|
const [profileLoading, setProfileLoading] = useState(true);
|
||||||
|
|
||||||
@ -102,10 +106,10 @@ function AuthBridge({ children }: { children: React.ReactNode }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: { session: nextSession } } = await supabase.auth.getSession();
|
const nextSession = await ensurePlatformSession();
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
const normalizedSession = (nextSession as Session | null) ?? null;
|
const normalizedSession = nextSession ?? null;
|
||||||
const normalizedUser = (normalizedSession?.user as User | null) ?? buildFallbackAuthUser(tradingAuth.user);
|
const normalizedUser = normalizedSession?.user ?? buildFallbackAuthUser(tradingAuth.user);
|
||||||
setSession(normalizedSession);
|
setSession(normalizedSession);
|
||||||
setUser(normalizedUser);
|
setUser(normalizedUser);
|
||||||
await fetchProfile(tradingAuth.user.id, normalizedUser);
|
await fetchProfile(tradingAuth.user.id, normalizedUser);
|
||||||
@ -118,7 +122,7 @@ function AuthBridge({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
}, [tradingAuth.user?.id]);
|
}, [tradingAuth.user?.id]);
|
||||||
|
|
||||||
const fetchProfile = async (_userId: string, authUserOverride?: User | null) => {
|
const fetchProfile = async (_userId: string, authUserOverride?: PlatformSessionUser | null) => {
|
||||||
try {
|
try {
|
||||||
const currentProfile = await fetchCurrentUserProfile();
|
const currentProfile = await fetchCurrentUserProfile();
|
||||||
setProfile(currentProfile as UserProfile);
|
setProfile(currentProfile as UserProfile);
|
||||||
@ -145,8 +149,8 @@ function AuthBridge({ children }: { children: React.ReactNode }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const signOut = async () => {
|
const signOut = async () => {
|
||||||
await supabase.auth.signOut();
|
|
||||||
tradingAuth.logout();
|
tradingAuth.logout();
|
||||||
|
clearPlatformSession();
|
||||||
setSession(null);
|
setSession(null);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setProfile(null);
|
setProfile(null);
|
||||||
@ -170,19 +174,18 @@ function AuthBridge({ children }: { children: React.ReactNode }) {
|
|||||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
if (!authUser?.id) return null;
|
||||||
return {
|
return {
|
||||||
id: authUser.id,
|
id: authUser.id,
|
||||||
email: authUser.email || '',
|
email: authUser.email || '',
|
||||||
aud: 'authenticated',
|
role: authUser.role || 'member',
|
||||||
app_metadata: {},
|
display_name: authUser.name || authUser.email || '',
|
||||||
user_metadata: {
|
user_metadata: {
|
||||||
role: authUser.role || 'member',
|
role: authUser.role || 'member',
|
||||||
displayName: authUser.name || authUser.email || '',
|
displayName: authUser.name || authUser.email || '',
|
||||||
},
|
},
|
||||||
created_at: new Date(0).toISOString(),
|
};
|
||||||
} as User;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
|
|||||||
@ -4,17 +4,13 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { ChatControl } from './ChatControl';
|
import { ChatControl } from './ChatControl';
|
||||||
|
|
||||||
const { getSessionMock, writeTextMock } = vi.hoisted(() => ({
|
const { getPlatformAccessTokenMock, writeTextMock } = vi.hoisted(() => ({
|
||||||
getSessionMock: vi.fn(),
|
getPlatformAccessTokenMock: vi.fn(),
|
||||||
writeTextMock: vi.fn()
|
writeTextMock: vi.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../lib/supabaseClient', () => ({
|
vi.mock('../lib/authSession', () => ({
|
||||||
supabase: {
|
getPlatformAccessToken: getPlatformAccessTokenMock
|
||||||
auth: {
|
|
||||||
getSession: getSessionMock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const profilesFixture = [
|
const profilesFixture = [
|
||||||
@ -24,7 +20,7 @@ const profilesFixture = [
|
|||||||
|
|
||||||
describe('ChatControl DOM flow', () => {
|
describe('ChatControl DOM flow', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getSessionMock.mockReset();
|
getPlatformAccessTokenMock.mockReset();
|
||||||
writeTextMock.mockReset();
|
writeTextMock.mockReset();
|
||||||
vi.stubGlobal('fetch', vi.fn());
|
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 () => {
|
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 onApplyProfile = vi.fn(async () => ({ success: true }));
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
@ -55,7 +51,7 @@ describe('ChatControl DOM flow', () => {
|
|||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
||||||
it('sends prompt, edits profile draft, copies JSON, and applies profile', async () => {
|
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);
|
const fetchMock = vi.mocked(fetch);
|
||||||
fetchMock.mockResolvedValue({
|
fetchMock.mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
@ -113,7 +109,7 @@ describe('ChatControl DOM flow', () => {
|
|||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
||||||
it('marks generated profile action as cancelled', async () => {
|
it('marks generated profile action as cancelled', async () => {
|
||||||
getSessionMock.mockResolvedValue({ data: { session: { access_token: 'token-2' } } });
|
getPlatformAccessTokenMock.mockResolvedValue('token-2');
|
||||||
vi.mocked(fetch).mockResolvedValue({
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => ({
|
json: async () => ({
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState, useRef, useEffect, useMemo } from 'react';
|
import { useState, useRef, useEffect, useMemo } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { supabase } from '../lib/supabaseClient';
|
|
||||||
import { tradingRuntime } from '../lib/runtime';
|
import { tradingRuntime } from '../lib/runtime';
|
||||||
|
import { getPlatformAccessToken } from '../lib/authSession';
|
||||||
import {
|
import {
|
||||||
Send, X, Bot, User,
|
Send, X, Bot, User,
|
||||||
Check, Loader2,
|
Check, Loader2,
|
||||||
@ -199,11 +199,7 @@ export const ChatControl = ({ profiles, onApplyProfile }: ChatControlProps) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const apiUrl = tradingRuntime.tradingApiUrl;
|
const apiUrl = tradingRuntime.tradingApiUrl;
|
||||||
const { data: sessionData } = await supabase.auth.getSession();
|
const accessToken = await getPlatformAccessToken();
|
||||||
const accessToken = sessionData.session?.access_token;
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error('Not authenticated');
|
|
||||||
}
|
|
||||||
const res = await fetch(`${apiUrl}/api/chat`, {
|
const res = await fetch(`${apiUrl}/api/chat`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@ -22,6 +22,7 @@ vi.mock('../lib/tradingAuth', () => ({
|
|||||||
useTradingAuth: () => ({
|
useTradingAuth: () => ({
|
||||||
login: vi.fn(async () => true),
|
login: vi.fn(async () => true),
|
||||||
register: vi.fn(async () => true),
|
register: vi.fn(async () => true),
|
||||||
|
forgotPassword: vi.fn(async () => true),
|
||||||
error: null,
|
error: null,
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -5,12 +5,12 @@ import userEvent from '@testing-library/user-event';
|
|||||||
import { EntryForm } from './EntryForm';
|
import { EntryForm } from './EntryForm';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getSessionMock,
|
getPlatformAccessTokenMock,
|
||||||
createManualEntryMock,
|
createManualEntryMock,
|
||||||
updateManualEntryMock,
|
updateManualEntryMock,
|
||||||
authMock
|
authMock
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
getSessionMock: vi.fn(),
|
getPlatformAccessTokenMock: vi.fn(),
|
||||||
createManualEntryMock: vi.fn(),
|
createManualEntryMock: vi.fn(),
|
||||||
updateManualEntryMock: vi.fn(),
|
updateManualEntryMock: vi.fn(),
|
||||||
authMock: { user: { id: 'user-1' } as any }
|
authMock: { user: { id: 'user-1' } as any }
|
||||||
@ -20,12 +20,8 @@ vi.mock('../components/AuthContext', () => ({
|
|||||||
useAuth: () => authMock
|
useAuth: () => authMock
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../lib/supabaseClient', () => ({
|
vi.mock('../lib/authSession', () => ({
|
||||||
supabase: {
|
getPlatformAccessToken: getPlatformAccessTokenMock
|
||||||
auth: {
|
|
||||||
getSession: getSessionMock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../lib/manualEntriesApi', () => ({
|
vi.mock('../lib/manualEntriesApi', () => ({
|
||||||
@ -40,12 +36,12 @@ describe('EntryForm DOM flow', () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
createManualEntryMock.mockReset();
|
createManualEntryMock.mockReset();
|
||||||
updateManualEntryMock.mockReset();
|
updateManualEntryMock.mockReset();
|
||||||
getSessionMock.mockReset();
|
getPlatformAccessTokenMock.mockReset();
|
||||||
authMock.user = { id: 'user-1' };
|
authMock.user = { id: 'user-1' };
|
||||||
|
|
||||||
createManualEntryMock.mockResolvedValue({});
|
createManualEntryMock.mockResolvedValue({});
|
||||||
updateManualEntryMock.mockResolvedValue({});
|
updateManualEntryMock.mockResolvedValue({});
|
||||||
getSessionMock.mockResolvedValue({ data: { session: null } });
|
getPlatformAccessTokenMock.mockRejectedValue(new Error('Not authenticated'));
|
||||||
|
|
||||||
vi.stubGlobal('fetch', vi.fn());
|
vi.stubGlobal('fetch', vi.fn());
|
||||||
vi.stubGlobal('confirm', vi.fn(() => true));
|
vi.stubGlobal('confirm', vi.fn(() => true));
|
||||||
@ -73,7 +69,7 @@ describe('EntryForm DOM flow', () => {
|
|||||||
it('alerts error when trade execution fails', async () => {
|
it('alerts error when trade execution fails', async () => {
|
||||||
const onSuccess = vi.fn();
|
const onSuccess = vi.fn();
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
getSessionMock.mockResolvedValue({ data: { session: { access_token: 'valid-token' } } });
|
getPlatformAccessTokenMock.mockResolvedValue('valid-token');
|
||||||
vi.mocked(fetch).mockResolvedValue({
|
vi.mocked(fetch).mockResolvedValue({
|
||||||
ok: false,
|
ok: false,
|
||||||
json: async () => ({ success: false, error: 'Insufficient funds' })
|
json: async () => ({ success: false, error: 'Insufficient funds' })
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import type { FormEvent } from 'react';
|
import type { FormEvent } from 'react';
|
||||||
import { supabase } from '../lib/supabaseClient';
|
|
||||||
import { useAuth } from '../components/AuthContext';
|
import { useAuth } from '../components/AuthContext';
|
||||||
import { tradingRuntime } from '../lib/runtime';
|
import { tradingRuntime } from '../lib/runtime';
|
||||||
import { createManualEntry, updateManualEntry } from '../lib/manualEntriesApi';
|
import { createManualEntry, updateManualEntry } from '../lib/manualEntriesApi';
|
||||||
|
import { getPlatformAccessToken } from '../lib/authSession';
|
||||||
|
|
||||||
interface EntryFormProps {
|
interface EntryFormProps {
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
@ -124,11 +124,7 @@ export function EntryForm({ onSuccess, initialData }: EntryFormProps) {
|
|||||||
if (!confirmTrade) return;
|
if (!confirmTrade) return;
|
||||||
|
|
||||||
const apiUrl = tradingRuntime.tradingApiUrl;
|
const apiUrl = tradingRuntime.tradingApiUrl;
|
||||||
const { data: sessionData } = await supabase.auth.getSession();
|
const accessToken = await getPlatformAccessToken();
|
||||||
const accessToken = sessionData.session?.access_token;
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error('Not authenticated');
|
|
||||||
}
|
|
||||||
const response = await fetch(`${apiUrl}/api/trade`, {
|
const response = await fetch(`${apiUrl}/api/trade`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@ -4,10 +4,10 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { Login } from './Login';
|
import { Login } from './Login';
|
||||||
|
|
||||||
const { loginMock, registerMock, resetPasswordForEmailMock, tradingAuthState } = vi.hoisted(() => ({
|
const { loginMock, registerMock, forgotPasswordMock, tradingAuthState } = vi.hoisted(() => ({
|
||||||
loginMock: vi.fn(),
|
loginMock: vi.fn(),
|
||||||
registerMock: vi.fn(),
|
registerMock: vi.fn(),
|
||||||
resetPasswordForEmailMock: vi.fn(),
|
forgotPasswordMock: vi.fn(),
|
||||||
tradingAuthState: {
|
tradingAuthState: {
|
||||||
error: null as string | null
|
error: null as string | null
|
||||||
}
|
}
|
||||||
@ -17,27 +17,20 @@ vi.mock('../lib/tradingAuth', () => ({
|
|||||||
useTradingAuth: () => ({
|
useTradingAuth: () => ({
|
||||||
login: loginMock,
|
login: loginMock,
|
||||||
register: registerMock,
|
register: registerMock,
|
||||||
|
forgotPassword: forgotPasswordMock,
|
||||||
error: tradingAuthState.error,
|
error: tradingAuthState.error,
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../lib/supabaseClient', () => ({
|
|
||||||
supabase: {
|
|
||||||
auth: {
|
|
||||||
resetPasswordForEmail: resetPasswordForEmailMock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('Login DOM flow', () => {
|
describe('Login DOM flow', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
loginMock.mockReset();
|
loginMock.mockReset();
|
||||||
registerMock.mockReset();
|
registerMock.mockReset();
|
||||||
resetPasswordForEmailMock.mockReset();
|
forgotPasswordMock.mockReset();
|
||||||
tradingAuthState.error = null;
|
tradingAuthState.error = null;
|
||||||
loginMock.mockResolvedValue(true);
|
loginMock.mockResolvedValue(true);
|
||||||
registerMock.mockResolvedValue(true);
|
registerMock.mockResolvedValue(true);
|
||||||
resetPasswordForEmailMock.mockResolvedValue({ error: null });
|
forgotPasswordMock.mockResolvedValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('submits sign-in credentials and surfaces auth errors', async () => {
|
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 user.click(screen.getByRole('button', { name: 'Send Reset Link' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(resetPasswordForEmailMock).toHaveBeenCalledWith(
|
expect(forgotPasswordMock).toHaveBeenCalledWith('recover@demo.com');
|
||||||
'recover@demo.com',
|
|
||||||
expect.objectContaining({
|
|
||||||
redirectTo: expect.stringContaining('/reset-callback')
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(screen.getByText('Password reset link sent! Check your email.')).toBeInTheDocument();
|
expect(screen.getByText('Password reset link sent! Check your email.')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { supabase } from '../lib/supabaseClient';
|
|
||||||
import { useTradingAuth } from '../lib/tradingAuth';
|
import { useTradingAuth } from '../lib/tradingAuth';
|
||||||
|
|
||||||
export function Login() {
|
export function Login() {
|
||||||
@ -20,10 +19,8 @@ export function Login() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (isResetPassword) {
|
if (isResetPassword) {
|
||||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
const ok = await tradingAuth.forgotPassword(email);
|
||||||
redirectTo: window.location.origin + '/reset-callback',
|
if (!ok) throw new Error(tradingAuth.error || 'Password reset failed');
|
||||||
});
|
|
||||||
if (error) throw error;
|
|
||||||
setMessage('Password reset link sent! Check your email.');
|
setMessage('Password reset link sent! Check your email.');
|
||||||
} else if (isSignUp) {
|
} else if (isSignUp) {
|
||||||
const ok = await tradingAuth.register(email, password, email.split('@')[0] || 'Trader');
|
const ok = await tradingAuth.register(email, password, email.split('@')[0] || 'Trader');
|
||||||
|
|||||||
@ -4,22 +4,18 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { ResetPassword } from './ResetPassword';
|
import { ResetPassword } from './ResetPassword';
|
||||||
|
|
||||||
const { updateUserMock } = vi.hoisted(() => ({
|
const { resetPlatformPasswordMock } = vi.hoisted(() => ({
|
||||||
updateUserMock: vi.fn()
|
resetPlatformPasswordMock: vi.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../lib/supabaseClient', () => ({
|
vi.mock('../lib/authSession', () => ({
|
||||||
supabase: {
|
resetPlatformPassword: resetPlatformPasswordMock
|
||||||
auth: {
|
|
||||||
updateUser: updateUserMock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('ResetPassword DOM flow', () => {
|
describe('ResetPassword DOM flow', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
updateUserMock.mockReset();
|
resetPlatformPasswordMock.mockReset();
|
||||||
updateUserMock.mockResolvedValue({ error: null });
|
resetPlatformPasswordMock.mockResolvedValue(undefined);
|
||||||
window.history.pushState({}, '', '/reset-callback#type=recovery');
|
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 user.click(screen.getByRole('button', { name: 'Update Password' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(updateUserMock).toHaveBeenCalledWith({ password: 'MyNewPassword123!' });
|
expect(resetPlatformPasswordMock).toHaveBeenCalledWith('MyNewPassword123!');
|
||||||
expect(screen.getByText('Password updated successfully! You can now login.')).toBeInTheDocument();
|
expect(screen.getByText('Password updated successfully! You can now login.')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
}, 15000);
|
}, 15000);
|
||||||
|
|
||||||
it('shows provider error when password update fails', async () => {
|
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();
|
const user = userEvent.setup();
|
||||||
render(<ResetPassword />);
|
render(<ResetPassword />);
|
||||||
|
|
||||||
@ -49,7 +45,7 @@ describe('ResetPassword DOM flow', () => {
|
|||||||
await user.click(screen.getByRole('button', { name: 'Update Password' }));
|
await user.click(screen.getByRole('button', { name: 'Update Password' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(updateUserMock).toHaveBeenCalled();
|
expect(resetPlatformPasswordMock).toHaveBeenCalled();
|
||||||
expect(screen.getByText('Password is too weak')).toBeInTheDocument();
|
expect(screen.getByText('Password is too weak')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { supabase } from '../lib/supabaseClient';
|
import { resetPlatformPassword } from '../lib/authSession';
|
||||||
|
|
||||||
export function ResetPassword() {
|
export function ResetPassword() {
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
@ -22,9 +22,8 @@ export function ResetPassword() {
|
|||||||
setError(null);
|
setError(null);
|
||||||
setMessage(null);
|
setMessage(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase.auth.updateUser({ password });
|
await resetPlatformPassword(password);
|
||||||
if (error) throw error;
|
|
||||||
setMessage('Password updated successfully! You can now login.');
|
setMessage('Password updated successfully! You can now login.');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
|||||||
@ -3,17 +3,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|||||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||||
import { useWebSocket } from './useWebSocket';
|
import { useWebSocket } from './useWebSocket';
|
||||||
|
|
||||||
const { getSessionMock, ioMock } = vi.hoisted(() => ({
|
const { getPlatformAccessTokenMock, ioMock } = vi.hoisted(() => ({
|
||||||
getSessionMock: vi.fn(),
|
getPlatformAccessTokenMock: vi.fn(),
|
||||||
ioMock: vi.fn()
|
ioMock: vi.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../lib/supabaseClient', () => ({
|
vi.mock('../lib/authSession', () => ({
|
||||||
supabase: {
|
getPlatformAccessToken: getPlatformAccessTokenMock
|
||||||
auth: {
|
|
||||||
getSession: getSessionMock
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('socket.io-client', () => ({
|
vi.mock('socket.io-client', () => ({
|
||||||
@ -26,7 +22,7 @@ describe('useWebSocket DOM/event behavior', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Object.keys(handlers).forEach((key) => delete handlers[key]);
|
Object.keys(handlers).forEach((key) => delete handlers[key]);
|
||||||
getSessionMock.mockReset();
|
getPlatformAccessTokenMock.mockReset();
|
||||||
ioMock.mockReset();
|
ioMock.mockReset();
|
||||||
|
|
||||||
socketStub = {
|
socketStub = {
|
||||||
@ -40,12 +36,12 @@ describe('useWebSocket DOM/event behavior', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('skips socket connection when there is no auth session token', async () => {
|
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'));
|
const { result } = renderHook(() => useWebSocket('http://localhost:5000'));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(getSessionMock).toHaveBeenCalledTimes(1);
|
expect(getPlatformAccessTokenMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(ioMock).not.toHaveBeenCalled();
|
expect(ioMock).not.toHaveBeenCalled();
|
||||||
@ -54,7 +50,7 @@ describe('useWebSocket DOM/event behavior', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('connects with token and applies websocket event updates', async () => {
|
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'));
|
const { result, unmount } = renderHook(() => useWebSocket('http://localhost:5000'));
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { io, Socket } from 'socket.io-client';
|
import { io, Socket } from 'socket.io-client';
|
||||||
import { buildTradingSocketOptions } from '../../../shared/realtime.js';
|
import { buildTradingSocketOptions } from '../../../shared/realtime.js';
|
||||||
import { supabase } from '../lib/supabaseClient';
|
import { getPlatformAccessToken } from '../lib/authSession';
|
||||||
|
|
||||||
export interface TradingControlSnapshot {
|
export interface TradingControlSnapshot {
|
||||||
mode: 'RUNNING' | 'PAUSED';
|
mode: 'RUNNING' | 'PAUSED';
|
||||||
@ -276,8 +276,7 @@ export const useWebSocket = (url: string) => {
|
|||||||
|
|
||||||
const connectSocket = async () => {
|
const connectSocket = async () => {
|
||||||
console.log('🔌 Attempting to connect to:', url);
|
console.log('🔌 Attempting to connect to:', url);
|
||||||
const { data } = await supabase.auth.getSession();
|
const token = await getPlatformAccessToken().catch(() => null);
|
||||||
const token = data.session?.access_token;
|
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
console.warn('Socket connection skipped: missing authenticated session token');
|
console.warn('Socket connection skipped: missing authenticated session token');
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const AUTH_STORAGE_PREFIX = 'invttrdg_web';
|
|||||||
const ACCESS_TOKEN_KEY = `${AUTH_STORAGE_PREFIX}_access_token`;
|
const ACCESS_TOKEN_KEY = `${AUTH_STORAGE_PREFIX}_access_token`;
|
||||||
const REFRESH_TOKEN_KEY = `${AUTH_STORAGE_PREFIX}_refresh_token`;
|
const REFRESH_TOKEN_KEY = `${AUTH_STORAGE_PREFIX}_refresh_token`;
|
||||||
const USER_KEY = `${AUTH_STORAGE_PREFIX}_auth_user`;
|
const USER_KEY = `${AUTH_STORAGE_PREFIX}_auth_user`;
|
||||||
|
const AUTH_CHANGE_EVENT = 'trading-platform-auth-change';
|
||||||
|
|
||||||
export interface PlatformSessionUser {
|
export interface PlatformSessionUser {
|
||||||
id: string;
|
id: string;
|
||||||
@ -18,6 +19,16 @@ export interface PlatformSession {
|
|||||||
user: PlatformSessionUser;
|
user: PlatformSessionUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PlatformAuthError extends Error {
|
||||||
|
status?: number;
|
||||||
|
|
||||||
|
constructor(message: string, status?: number) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'PlatformAuthError';
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseJson<T>(value: string | null): T | null {
|
function parseJson<T>(value: string | null): T | null {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
try {
|
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<string, any> | 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<T>(
|
||||||
|
path: string,
|
||||||
|
options?: {
|
||||||
|
method?: string;
|
||||||
|
accessToken?: string;
|
||||||
|
body?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
): Promise<T> {
|
||||||
|
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<PlatformSessionUser> {
|
||||||
|
const me = await platformRequest<any>('/auth/me', { accessToken });
|
||||||
|
return normalizeUser(me);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshPlatformSession(refreshToken: string): Promise<PlatformSession> {
|
||||||
|
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<PlatformSession | null> {
|
||||||
|
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<string> {
|
||||||
|
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 session = getStoredPlatformSession();
|
||||||
const accessToken = session?.access_token;
|
const accessToken = session?.access_token;
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
@ -50,3 +220,27 @@ export function getPlatformAccessToken(): string {
|
|||||||
}
|
}
|
||||||
return accessToken;
|
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<void> {
|
||||||
|
const token = getPasswordResetToken();
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('Missing password reset token');
|
||||||
|
}
|
||||||
|
await platformRequest('/auth/reset-password', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
token,
|
||||||
|
newPassword,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { supabase } from './supabaseClient';
|
import { getPlatformAccessToken } from './authSession';
|
||||||
import { tradingRuntime } from './runtime';
|
import { tradingRuntime } from './runtime';
|
||||||
|
|
||||||
export interface DynamicConfigItem {
|
export interface DynamicConfigItem {
|
||||||
@ -7,17 +7,8 @@ export interface DynamicConfigItem {
|
|||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAccessToken(): Promise<string> {
|
|
||||||
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<DynamicConfigItem[]> {
|
export async function fetchDynamicConfigItems(): Promise<DynamicConfigItem[]> {
|
||||||
const accessToken = await getAccessToken();
|
const accessToken = await getPlatformAccessToken();
|
||||||
const response = await fetch(`${tradingRuntime.tradingApiUrl}/api/admin/config/dynamic`, {
|
const response = await fetch(`${tradingRuntime.tradingApiUrl}/api/admin/config/dynamic`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
@ -33,7 +24,7 @@ export async function fetchDynamicConfigItems(): Promise<DynamicConfigItem[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function upsertDynamicConfigItems(items: DynamicConfigItem[]): Promise<void> {
|
export async function upsertDynamicConfigItems(items: DynamicConfigItem[]): Promise<void> {
|
||||||
const accessToken = await getAccessToken();
|
const accessToken = await getPlatformAccessToken();
|
||||||
const response = await fetch(`${tradingRuntime.tradingApiUrl}/api/admin/config/dynamic`, {
|
const response = await fetch(`${tradingRuntime.tradingApiUrl}/api/admin/config/dynamic`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@ -1,37 +1,7 @@
|
|||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
import { getWebSupabaseConfig } from '../../../shared/supabase-config.js';
|
import { getWebSupabaseConfig } from '../../../shared/supabase-config.js';
|
||||||
import { getRuntimeEnvironment } from '../../../shared/runtime.js';
|
|
||||||
|
|
||||||
const supabaseConfig = getWebSupabaseConfig();
|
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<string, unknown>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
class PlatformAuthError extends Error {
|
|
||||||
status?: number;
|
|
||||||
|
|
||||||
constructor(message: string, status?: number) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'PlatformAuthError';
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!supabaseConfig.isConfigured) {
|
if (!supabaseConfig.isConfigured) {
|
||||||
console.warn('Missing Supabase environment variables for legacy data client fallback');
|
console.warn('Missing Supabase environment variables for legacy data client fallback');
|
||||||
@ -41,287 +11,6 @@ const dataClient = supabaseConfig.isConfigured
|
|||||||
? createClient(supabaseConfig.url, supabaseConfig.anonKey)
|
? createClient(supabaseConfig.url, supabaseConfig.anonKey)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
function parseJson<T>(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<PlatformSession['user']>(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<string, any> | 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<T>(
|
|
||||||
path: string,
|
|
||||||
options?: {
|
|
||||||
method?: string;
|
|
||||||
accessToken?: string;
|
|
||||||
body?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
): Promise<T> {
|
|
||||||
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<PlatformSession['user']> {
|
|
||||||
const me = await platformRequest<any>('/auth/me', { accessToken });
|
|
||||||
return normalizeUser(me);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshPlatformSession(refreshToken: string): Promise<PlatformSession> {
|
|
||||||
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<PlatformSession | null> {
|
|
||||||
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 = {
|
export const supabase = {
|
||||||
from: (...args: any[]) => {
|
from: (...args: any[]) => {
|
||||||
if (!dataClient) {
|
if (!dataClient) {
|
||||||
@ -329,5 +18,4 @@ export const supabase = {
|
|||||||
}
|
}
|
||||||
return (dataClient.from as any)(...args);
|
return (dataClient.from as any)(...args);
|
||||||
},
|
},
|
||||||
auth,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,12 +10,12 @@ import {
|
|||||||
ChevronRight, Pause, Play, AlertTriangle,
|
ChevronRight, Pause, Play, AlertTriangle,
|
||||||
Database, RefreshCcw, Heart, Info, XCircle
|
Database, RefreshCcw, Heart, Info, XCircle
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { supabase } from '../lib/supabaseClient';
|
|
||||||
import type { BotState } from '../hooks/useWebSocket';
|
import type { BotState } from '../hooks/useWebSocket';
|
||||||
import { useWebSocket } from '../hooks/useWebSocket';
|
import { useWebSocket } from '../hooks/useWebSocket';
|
||||||
import { useAuth } from '../components/AuthContext';
|
import { useAuth } from '../components/AuthContext';
|
||||||
import { tradingRuntime } from '../lib/runtime';
|
import { tradingRuntime } from '../lib/runtime';
|
||||||
import { fetchDynamicConfigItems, upsertDynamicConfigItems } from '../lib/dynamicConfigApi';
|
import { fetchDynamicConfigItems, upsertDynamicConfigItems } from '../lib/dynamicConfigApi';
|
||||||
|
import { getPlatformAccessToken } from '../lib/authSession';
|
||||||
|
|
||||||
interface AdminTabProps {
|
interface AdminTabProps {
|
||||||
botState: BotState;
|
botState: BotState;
|
||||||
@ -153,11 +153,7 @@ export const AdminTab = ({ botState }: AdminTabProps) => {
|
|||||||
setControlError(null);
|
setControlError(null);
|
||||||
try {
|
try {
|
||||||
const apiUrl = tradingRuntime.tradingApiUrl;
|
const apiUrl = tradingRuntime.tradingApiUrl;
|
||||||
const { data: sessionData } = await supabase.auth.getSession();
|
const accessToken = await getPlatformAccessToken();
|
||||||
const accessToken = sessionData.session?.access_token;
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error('Not authenticated');
|
|
||||||
}
|
|
||||||
const res = await fetch(`${apiUrl}/internal/trading/pause`, {
|
const res = await fetch(`${apiUrl}/internal/trading/pause`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -182,11 +178,7 @@ export const AdminTab = ({ botState }: AdminTabProps) => {
|
|||||||
setControlError(null);
|
setControlError(null);
|
||||||
try {
|
try {
|
||||||
const apiUrl = tradingRuntime.tradingApiUrl;
|
const apiUrl = tradingRuntime.tradingApiUrl;
|
||||||
const { data: sessionData } = await supabase.auth.getSession();
|
const accessToken = await getPlatformAccessToken();
|
||||||
const accessToken = sessionData.session?.access_token;
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error('Not authenticated');
|
|
||||||
}
|
|
||||||
const res = await fetch(`${apiUrl}/internal/trading/resume`, {
|
const res = await fetch(`${apiUrl}/internal/trading/resume`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -211,11 +203,7 @@ export const AdminTab = ({ botState }: AdminTabProps) => {
|
|||||||
setControlError(null);
|
setControlError(null);
|
||||||
try {
|
try {
|
||||||
const apiUrl = tradingRuntime.tradingApiUrl;
|
const apiUrl = tradingRuntime.tradingApiUrl;
|
||||||
const { data: sessionData } = await supabase.auth.getSession();
|
const accessToken = await getPlatformAccessToken();
|
||||||
const accessToken = sessionData.session?.access_token;
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error('Not authenticated');
|
|
||||||
}
|
|
||||||
const res = await fetch(`${apiUrl}/api/events`, {
|
const res = await fetch(`${apiUrl}/api/events`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
@ -238,8 +226,7 @@ export const AdminTab = ({ botState }: AdminTabProps) => {
|
|||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const apiUrl = tradingRuntime.tradingApiUrl;
|
const apiUrl = tradingRuntime.tradingApiUrl;
|
||||||
const { data: sessionData } = await supabase.auth.getSession();
|
const accessToken = await getPlatformAccessToken().catch(() => null);
|
||||||
const accessToken = sessionData.session?.access_token;
|
|
||||||
if (!accessToken) return;
|
if (!accessToken) return;
|
||||||
const res = await fetch(`${apiUrl}/api/config`, {
|
const res = await fetch(`${apiUrl}/api/config`, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AlertTriangle, Clock3, RefreshCcw, Search, ShieldCheck, Undo2 } from 'lucide-react';
|
import { AlertTriangle, Clock3, RefreshCcw, Search, ShieldCheck, Undo2 } from 'lucide-react';
|
||||||
import { supabase } from '../lib/supabaseClient';
|
|
||||||
import { tradingRuntime } from '../lib/runtime';
|
import { tradingRuntime } from '../lib/runtime';
|
||||||
|
import { getPlatformAccessToken } from '../lib/authSession';
|
||||||
|
|
||||||
interface ReconciliationBackfillAuditRow {
|
interface ReconciliationBackfillAuditRow {
|
||||||
id: number;
|
id: number;
|
||||||
@ -158,11 +158,7 @@ export const ReconciliationAuditPanel = () => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const { data: sessionData } = await supabase.auth.getSession();
|
const accessToken = await getPlatformAccessToken();
|
||||||
const accessToken = sessionData.session?.access_token;
|
|
||||||
if (!accessToken) {
|
|
||||||
throw new Error('Not authenticated');
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiUrl = tradingRuntime.tradingApiUrl;
|
const apiUrl = tradingRuntime.tradingApiUrl;
|
||||||
const auditParams = buildQueryParams(filters, PAGE_LIMIT, offset);
|
const auditParams = buildQueryParams(filters, PAGE_LIMIT, offset);
|
||||||
@ -262,9 +258,7 @@ export const ReconciliationAuditPanel = () => {
|
|||||||
setIsReverting(batchId);
|
setIsReverting(batchId);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const { data: sessionData } = await supabase.auth.getSession();
|
const accessToken = await getPlatformAccessToken();
|
||||||
const accessToken = sessionData.session?.access_token;
|
|
||||||
if (!accessToken) throw new Error('Not authenticated');
|
|
||||||
|
|
||||||
const apiUrl = tradingRuntime.tradingApiUrl;
|
const apiUrl = tradingRuntime.tradingApiUrl;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user