196 lines
6.7 KiB
TypeScript
196 lines
6.7 KiB
TypeScript
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';
|
|
|
|
// Define the shape of our extended user profile
|
|
export interface UserProfile {
|
|
user_id: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
email: string;
|
|
role: string;
|
|
|
|
// Alpaca Settings
|
|
ALPACA_API_KEY?: string;
|
|
ALPACA_SECRET_KEY?: string;
|
|
REAL_ALPACA_API_KEY?: string;
|
|
REAL_ALPACA_SECRET_KEY?: string;
|
|
|
|
// Bot Settings
|
|
trade_enable: boolean;
|
|
drop_threshold_for_buy?: string | number;
|
|
gain_threshold_for_sell?: string | number;
|
|
market_poll_interval_in_seconds?: string | number;
|
|
}
|
|
|
|
interface AuthContextType {
|
|
session: Session | null;
|
|
user: User | null;
|
|
profile: UserProfile | null;
|
|
loading: boolean;
|
|
signOut: () => Promise<void>;
|
|
refreshProfile: () => Promise<void>;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
|
|
const buildFallbackProfile = (authUser: User | null): UserProfile | null => {
|
|
if (!authUser?.id) return null;
|
|
const displayName = String((authUser as any)?.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'),
|
|
trade_enable: true,
|
|
};
|
|
};
|
|
|
|
export const shouldCreateDefaultProfile = (profiles: Array<{ id?: string }> | null | undefined) =>
|
|
!profiles || profiles.length === 0;
|
|
|
|
export const buildDefaultProfilePayload = (userId: string) => ({
|
|
user_id: userId,
|
|
name: 'My First Strategy',
|
|
allocated_capital: 1000,
|
|
risk_per_trade_percent: 1,
|
|
symbols: 'BTC/USDT, ETH/USDT',
|
|
is_active: false,
|
|
strategy_config: {
|
|
rules: [
|
|
{ ruleId: 'TrendBiasRule', enabled: true, params: { fastPeriod: 50, slowPeriod: 200 } },
|
|
{ ruleId: 'MomentumRule', enabled: true, params: { rsiPeriod: 14, overbought: 70, oversold: 30 } },
|
|
{ ruleId: 'ZoneRule', enabled: true, params: { zonePercent: 1.5 } },
|
|
{ ruleId: 'SessionRule', enabled: true, params: { sessions: 'London,NY' } },
|
|
{ ruleId: 'EntryTriggerRule', enabled: true, params: { showPatterns: true } },
|
|
{ ruleId: 'RiskManagementRule', enabled: true, params: { maxRisk: 2.0 } },
|
|
{ ruleId: 'AIAnalysisRule', enabled: false, params: { minConfidence: 0.7 } },
|
|
],
|
|
riskLimits: { maxDailyLossUsd: 50, maxOpenTrades: 3, maxConsecutiveLosses: 2 },
|
|
execution: { orderType: 'market', cooldownMinutes: 30, entryMode: 'both' },
|
|
},
|
|
});
|
|
|
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<TradingAuthProvider>
|
|
<AuthBridge>{children}</AuthBridge>
|
|
</TradingAuthProvider>
|
|
);
|
|
}
|
|
|
|
function AuthBridge({ children }: { children: React.ReactNode }) {
|
|
const tradingAuth = useTradingAuth();
|
|
const [session, setSession] = useState<Session | null>(null);
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
|
const [profileLoading, setProfileLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
let active = true;
|
|
const syncSession = async () => {
|
|
if (!tradingAuth.user?.id) {
|
|
if (!active) return;
|
|
setSession(null);
|
|
setUser(null);
|
|
setProfile(null);
|
|
setProfileLoading(false);
|
|
return;
|
|
}
|
|
|
|
const { data: { session: nextSession } } = await supabase.auth.getSession();
|
|
if (!active) return;
|
|
const normalizedSession = (nextSession as Session | null) ?? null;
|
|
const normalizedUser = (normalizedSession?.user as User | null) ?? buildFallbackAuthUser(tradingAuth.user);
|
|
setSession(normalizedSession);
|
|
setUser(normalizedUser);
|
|
await fetchProfile(tradingAuth.user.id, normalizedUser);
|
|
};
|
|
|
|
void syncSession();
|
|
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, [tradingAuth.user?.id]);
|
|
|
|
const fetchProfile = async (_userId: string, authUserOverride?: User | null) => {
|
|
try {
|
|
const currentProfile = await fetchCurrentUserProfile();
|
|
setProfile(currentProfile as UserProfile);
|
|
await ensureDefaultProfile();
|
|
} catch (err) {
|
|
console.error('Unexpected error fetching profile:', err);
|
|
setProfile(buildFallbackProfile(authUserOverride ?? user));
|
|
} finally {
|
|
setProfileLoading(false);
|
|
}
|
|
};
|
|
|
|
const ensureDefaultProfile = async () => {
|
|
try {
|
|
const profiles = await fetchTradeProfiles({ ensureDefault: true });
|
|
if (shouldCreateDefaultProfile(profiles)) {
|
|
console.log('[Auth] No profiles found after bootstrap ensureDefault call');
|
|
} else {
|
|
window.dispatchEvent(new Event('profiles-updated'));
|
|
}
|
|
} catch (err) {
|
|
console.error('[Auth] Error ensuring default profile:', err);
|
|
}
|
|
};
|
|
|
|
const signOut = async () => {
|
|
await supabase.auth.signOut();
|
|
tradingAuth.logout();
|
|
setSession(null);
|
|
setUser(null);
|
|
setProfile(null);
|
|
};
|
|
|
|
const refreshProfile = async () => {
|
|
if (user) {
|
|
await fetchProfile(user.id);
|
|
}
|
|
}
|
|
|
|
const value = {
|
|
session,
|
|
user,
|
|
profile,
|
|
loading: tradingAuth.isLoading || profileLoading,
|
|
signOut,
|
|
refreshProfile
|
|
};
|
|
|
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
}
|
|
|
|
const buildFallbackAuthUser = (authUser: { id: string; email?: string; role?: string; name?: string; } | null): User | null => {
|
|
if (!authUser?.id) return null;
|
|
return {
|
|
id: authUser.id,
|
|
email: authUser.email || '',
|
|
aud: 'authenticated',
|
|
app_metadata: {},
|
|
user_metadata: {
|
|
role: authUser.role || 'member',
|
|
displayName: authUser.name || authUser.email || '',
|
|
},
|
|
created_at: new Date(0).toISOString(),
|
|
} as User;
|
|
};
|
|
|
|
export const useAuth = () => {
|
|
const context = useContext(AuthContext);
|
|
if (context === undefined) {
|
|
throw new Error('useAuth must be used within an AuthProvider');
|
|
}
|
|
return context;
|
|
};
|
|
|