diff --git a/backend/src/services/apiServer.ts b/backend/src/services/apiServer.ts index e7fb248..391090e 100644 --- a/backend/src/services/apiServer.ts +++ b/backend/src/services/apiServer.ts @@ -139,6 +139,16 @@ const getConfiguredFmpApiKey = (): string => { return apiKey; }; +async function getUserFmpApiKey(userId: string): Promise { + const profile = await getCurrentUserProfile(userId); + const profileApiKey = String(profile.FMP_API_KEY || '').trim(); + if (profileApiKey) { + return profileApiKey; + } + + return getConfiguredFmpApiKey(); +} + class MissingServiceConfigError extends Error { constructor(message: string) { super(message); @@ -2867,9 +2877,11 @@ RULES: // ── Research: company profile from FMP ─────────────────────────────── this.app.get('/api/research/profile', this.requireAuth, async (req, res) => { try { + const authUserId = (req as AuthenticatedRequest).authUserId; + if (!authUserId) return res.status(401).json({ error: 'Unauthorized' }); const symbol = String(req.query.symbol || '').trim().toUpperCase(); if (!symbol) return res.status(400).json({ error: 'symbol required' }); - const apiKey = getConfiguredFmpApiKey(); + const apiKey = await getUserFmpApiKey(authUserId); const url = `https://financialmodelingprep.com/api/v3/profile/${symbol}?apikey=${apiKey}`; const data = await fetchFmpJson(url) as any; res.json(Array.isArray(data) ? data[0] ?? {} : data); @@ -2887,9 +2899,11 @@ RULES: // ── Research: key metrics (P/E, ROE, etc.) from FMP ────────────────── this.app.get('/api/research/metrics', this.requireAuth, async (req, res) => { try { + const authUserId = (req as AuthenticatedRequest).authUserId; + if (!authUserId) return res.status(401).json({ error: 'Unauthorized' }); const symbol = String(req.query.symbol || '').trim().toUpperCase(); if (!symbol) return res.status(400).json({ error: 'symbol required' }); - const apiKey = getConfiguredFmpApiKey(); + const apiKey = await getUserFmpApiKey(authUserId); const url = `https://financialmodelingprep.com/api/v3/key-metrics/${symbol}?limit=4&apikey=${apiKey}`; const data = await fetchFmpJson(url) as any; res.json(Array.isArray(data) ? data[0] ?? {} : data); @@ -2907,9 +2921,11 @@ RULES: // ── Research: earnings calendar from FMP ────────────────────────────── this.app.get('/api/research/earnings', this.requireAuth, async (req, res) => { try { + const authUserId = (req as AuthenticatedRequest).authUserId; + if (!authUserId) return res.status(401).json({ error: 'Unauthorized' }); const symbol = String(req.query.symbol || '').trim().toUpperCase(); if (!symbol) return res.status(400).json({ error: 'symbol required' }); - const apiKey = getConfiguredFmpApiKey(); + const apiKey = await getUserFmpApiKey(authUserId); const url = `https://financialmodelingprep.com/api/v3/historical/earning_calendar/${symbol}?limit=8&apikey=${apiKey}`; const data = await fetchFmpJson(url) as any; res.json({ earnings: Array.isArray(data) ? data : [] }); @@ -2927,7 +2943,11 @@ RULES: // ── Screener: stock screener from FMP ──────────────────────────────── this.app.get('/api/screener', this.requireAuth, async (req, res) => { try { - const apiKey = getConfiguredFmpApiKey(); + const authUserId = (req as AuthenticatedRequest).authUserId; + if (!authUserId) { + return res.status(401).json({ error: 'Unauthorized' }); + } + const apiKey = await getUserFmpApiKey(authUserId); const qs = new URLSearchParams(); const sector = String(req.query.sector || '').trim(); if (sector && sector !== 'All') { diff --git a/backend/src/services/profileRepository.ts b/backend/src/services/profileRepository.ts index 8361bdc..b778116 100644 --- a/backend/src/services/profileRepository.ts +++ b/backend/src/services/profileRepository.ts @@ -11,6 +11,7 @@ export interface TradingUserProfile { email: string; role: string; trade_enable: boolean; + FMP_API_KEY?: string; ALPACA_API_KEY?: string; ALPACA_SECRET_KEY?: string; REAL_ALPACA_API_KEY?: string; @@ -132,6 +133,7 @@ function normalizeTradingUserProfile( email: String(row?.email || ''), role: String(row?.role || 'member'), trade_enable: Boolean(row?.trade_enable ?? true), + FMP_API_KEY: row?.FMP_API_KEY, ALPACA_API_KEY: row?.ALPACA_API_KEY, ALPACA_SECRET_KEY: row?.ALPACA_SECRET_KEY, REAL_ALPACA_API_KEY: row?.REAL_ALPACA_API_KEY, @@ -565,7 +567,7 @@ export async function getCurrentUserProfile( try { const { data, error } = await client .from('users') - .select('user_id,first_name,last_name,email,role,trade_enable,ALPACA_API_KEY,ALPACA_SECRET_KEY,REAL_ALPACA_API_KEY,REAL_ALPACA_SECRET_KEY,drop_threshold_for_buy,gain_threshold_for_sell,market_poll_interval_in_seconds') + .select('user_id,first_name,last_name,email,role,trade_enable,FMP_API_KEY,ALPACA_API_KEY,ALPACA_SECRET_KEY,REAL_ALPACA_API_KEY,REAL_ALPACA_SECRET_KEY,drop_threshold_for_buy,gain_threshold_for_sell,market_poll_interval_in_seconds') .eq('user_id', userId) .maybeSingle(); @@ -577,6 +579,7 @@ export async function getCurrentUserProfile( email: String((data as any).email || fallback.email || ''), role: String((data as any).role || fallback.role || 'member'), trade_enable: Boolean((data as any).trade_enable ?? fallback.trade_enable ?? true), + FMP_API_KEY: (data as any).FMP_API_KEY || fallback.FMP_API_KEY, ALPACA_API_KEY: (data as any).ALPACA_API_KEY || fallback.ALPACA_API_KEY, ALPACA_SECRET_KEY: (data as any).ALPACA_SECRET_KEY || fallback.ALPACA_SECRET_KEY, REAL_ALPACA_API_KEY: (data as any).REAL_ALPACA_API_KEY || fallback.REAL_ALPACA_API_KEY, @@ -600,6 +603,7 @@ export async function getCurrentUserProfile( email: String(fallback.email || ''), role: String(fallback.role || 'member'), trade_enable: Boolean(fallback.trade_enable ?? true), + FMP_API_KEY: fallback.FMP_API_KEY, ALPACA_API_KEY: fallback.ALPACA_API_KEY, ALPACA_SECRET_KEY: fallback.ALPACA_SECRET_KEY, REAL_ALPACA_API_KEY: fallback.REAL_ALPACA_API_KEY, @@ -648,6 +652,7 @@ export async function saveCurrentUserProfile( email: merged.email, role: merged.role, trade_enable: merged.trade_enable, + FMP_API_KEY: merged.FMP_API_KEY ?? null, ALPACA_API_KEY: merged.ALPACA_API_KEY ?? null, ALPACA_SECRET_KEY: merged.ALPACA_SECRET_KEY ?? null, REAL_ALPACA_API_KEY: merged.REAL_ALPACA_API_KEY ?? null, diff --git a/backend/src/services/tradingUserTypes.ts b/backend/src/services/tradingUserTypes.ts index 336b3f3..de0fa7d 100644 --- a/backend/src/services/tradingUserTypes.ts +++ b/backend/src/services/tradingUserTypes.ts @@ -7,6 +7,7 @@ export interface UserConfig { first_name: string; last_name: string; email: string; + FMP_API_KEY: string; ALPACA_API_KEY: string; ALPACA_SECRET_KEY: string; REAL_ALPACA_API_KEY: string; diff --git a/backend/src/services/userRepository.ts b/backend/src/services/userRepository.ts index 3d8b2db..a90cc9e 100644 --- a/backend/src/services/userRepository.ts +++ b/backend/src/services/userRepository.ts @@ -27,6 +27,7 @@ function normalizeUser(row: Partial | null | undefined): UserConfig first_name: String(row?.first_name || ''), last_name: String(row?.last_name || ''), email: String(row?.email || ''), + FMP_API_KEY: String(row?.FMP_API_KEY || ''), ALPACA_API_KEY: String(row?.ALPACA_API_KEY || ''), ALPACA_SECRET_KEY: String(row?.ALPACA_SECRET_KEY || ''), REAL_ALPACA_API_KEY: String(row?.REAL_ALPACA_API_KEY || ''), diff --git a/web/src/components/AuthContext.tsx b/web/src/components/AuthContext.tsx index e11536e..8aff25a 100644 --- a/web/src/components/AuthContext.tsx +++ b/web/src/components/AuthContext.tsx @@ -16,6 +16,9 @@ export interface UserProfile { email: string; role: string; + // FMP Settings + FMP_API_KEY?: string; + // Alpaca Settings ALPACA_API_KEY?: string; ALPACA_SECRET_KEY?: string; diff --git a/web/src/lib/profileApi.ts b/web/src/lib/profileApi.ts index 7b7eb84..2281fff 100644 --- a/web/src/lib/profileApi.ts +++ b/web/src/lib/profileApi.ts @@ -22,6 +22,7 @@ export interface CurrentUserProfile { email: string; role: string; trade_enable: boolean; + FMP_API_KEY?: string; ALPACA_API_KEY?: string; ALPACA_SECRET_KEY?: string; REAL_ALPACA_API_KEY?: string; diff --git a/web/src/tabs/SettingsTab.tsx b/web/src/tabs/SettingsTab.tsx index 4111d7c..2db7890 100644 --- a/web/src/tabs/SettingsTab.tsx +++ b/web/src/tabs/SettingsTab.tsx @@ -10,6 +10,7 @@ interface SettingsTabProps { export interface SettingsFormData { first_name: string; last_name: string; + FMP_API_KEY: string; ALPACA_API_KEY: string; ALPACA_SECRET_KEY: string; REAL_ALPACA_API_KEY: string; @@ -23,6 +24,7 @@ export interface SettingsFormData { export const DEFAULT_SETTINGS_FORM_DATA: SettingsFormData = { first_name: '', last_name: '', + FMP_API_KEY: '', ALPACA_API_KEY: '', ALPACA_SECRET_KEY: '', REAL_ALPACA_API_KEY: '', @@ -36,6 +38,7 @@ export const DEFAULT_SETTINGS_FORM_DATA: SettingsFormData = { export const mapProfileToFormData = (profile?: any): SettingsFormData => ({ first_name: profile?.first_name || '', last_name: profile?.last_name || '', + FMP_API_KEY: profile?.FMP_API_KEY || '', ALPACA_API_KEY: profile?.ALPACA_API_KEY || '', ALPACA_SECRET_KEY: profile?.ALPACA_SECRET_KEY || '', REAL_ALPACA_API_KEY: profile?.REAL_ALPACA_API_KEY || '', @@ -92,6 +95,7 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => { await updateCurrentUserProfile({ first_name: formData.first_name, last_name: formData.last_name, + FMP_API_KEY: formData.FMP_API_KEY, ALPACA_API_KEY: formData.ALPACA_API_KEY, ALPACA_SECRET_KEY: formData.ALPACA_SECRET_KEY, REAL_ALPACA_API_KEY: formData.REAL_ALPACA_API_KEY, @@ -214,6 +218,36 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => { {/* Break */} +
+
+ Research & Screener Credentials +
+ +
+ + +
+
+
+ + Used for screener, company profile, key metrics, and earnings data. + + + Get your Financial Modeling Prep API key + +
+
+

Alpaca Credentials @@ -330,6 +364,21 @@ export const SettingsTab = ({ botState }: SettingsTabProps) => { gap: 10px; height: 40px; } + .settings-hint { + display: flex; + flex-direction: column; + gap: 8px; + color: #9aa0a6; + font-size: 0.9rem; + line-height: 1.5; + } + .settings-hint a { + color: #00ff88; + text-decoration: none; + } + .settings-hint a:hover { + text-decoration: underline; + } `}
);