learning_ai_invt_trdg/web/src/components/AuthContext.tsx

186 lines
6.5 KiB
TypeScript

import React, { createContext, useContext, useEffect, useState } from 'react';
import type { User, Session } from '@supabase/supabase-js';
import { supabase } from '../lib/supabaseClient';
import { tableNameUsers, tableNameProfiles } from '../lib/const';
// 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 }) {
const [session, setSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(null);
const [profile, setProfile] = useState<UserProfile | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 1. Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setSession((session as Session | null) ?? null);
setUser((session?.user as User | null) ?? null);
if (session?.user) {
fetchProfile(session.user.id);
} else {
setLoading(false);
}
});
// 2. Listen for changes
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setSession((session as Session | null) ?? null);
setUser((session?.user as User | null) ?? null);
if (session?.user) {
fetchProfile(session.user.id);
} else {
setProfile(null);
setLoading(false);
}
});
return () => subscription.unsubscribe();
}, []);
const fetchProfile = async (userId: string) => {
try {
const { data, error } = await supabase
.from(tableNameUsers)
.select('user_id,first_name,last_name,email,role,ALPACA_API_KEY,ALPACA_SECRET_KEY,REAL_ALPACA_API_KEY,REAL_ALPACA_SECRET_KEY,trade_enable,drop_threshold_for_buy,gain_threshold_for_sell,market_poll_interval_in_seconds')
.eq('user_id', userId)
.single();
if (error) {
console.error('Error fetching user profile:', error);
setProfile(buildFallbackProfile(user));
ensureDefaultProfile(userId);
} else {
setProfile(data as UserProfile);
// Ensure a default trading profile exists for new users
ensureDefaultProfile(userId);
}
} catch (err) {
console.error('Unexpected error fetching profile:', err);
setProfile(buildFallbackProfile(user));
} finally {
setLoading(false);
}
};
const ensureDefaultProfile = async (userId: string) => {
try {
const { data } = await supabase
.from(tableNameProfiles)
.select('id')
.eq('user_id', userId)
.limit(1);
if (shouldCreateDefaultProfile(data)) {
console.log('[Auth] No profiles found - creating default profile for new user');
await supabase.from(tableNameProfiles).insert([buildDefaultProfilePayload(userId)]);
window.dispatchEvent(new Event('profiles-updated'));
}
} catch (err) {
console.error('[Auth] Error ensuring default profile:', err);
}
};
const signOut = async () => {
await supabase.auth.signOut();
setSession(null);
setUser(null);
setProfile(null);
};
const refreshProfile = async () => {
if (user) {
await fetchProfile(user.id);
}
}
const value = {
session,
user,
profile,
loading,
signOut,
refreshProfile
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};