From 351412423f05e7a9d8d187b2f1b75775f7fa6f92 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 5 May 2026 22:40:06 +0000 Subject: [PATCH] fix(api): use user alpaca keys for market data --- backend/src/services/apiServer.ts | 66 ++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/backend/src/services/apiServer.ts b/backend/src/services/apiServer.ts index 61433d6..41b9ed6 100644 --- a/backend/src/services/apiServer.ts +++ b/backend/src/services/apiServer.ts @@ -146,6 +146,27 @@ class MissingServiceConfigError extends Error { } } +interface AlpacaCredentials { + key: string; + secret: string; +} + +async function getUserAlpacaCredentials(userId: string): Promise { + const profile = await getCurrentUserProfile(userId); + const key = String(config.PAPER_TRADING ? profile.ALPACA_API_KEY || '' : profile.REAL_ALPACA_API_KEY || '').trim(); + const secret = String(config.PAPER_TRADING ? profile.ALPACA_SECRET_KEY || '' : profile.REAL_ALPACA_SECRET_KEY || '').trim(); + + if (!key || !secret) { + throw new MissingServiceConfigError( + config.PAPER_TRADING + ? 'User Alpaca paper credentials are not configured' + : 'User Alpaca live credentials are not configured' + ); + } + + return { key, secret }; +} + interface TradeAuditEvent { event: string; userId?: string; @@ -2683,12 +2704,12 @@ RULES: // ── Chart bars: Alpaca stock bars with period mapping ──────────────── this.app.get('/api/chart/bars', this.requireAuth, async (req, res) => { try { + const authUserId = (req as AuthenticatedRequest).authUserId; const symbol = String(req.query.symbol || '').trim().toUpperCase(); const period = String(req.query.period || '1Y').trim(); - const alpacaKey = config.ALPACA_API_KEY; - const alpacaSecret = config.ALPACA_API_SECRET; + if (!authUserId) return res.status(401).json({ error: 'Unauthorized' }); if (!symbol) return res.status(400).json({ error: 'symbol required' }); - if (!alpacaKey || !alpacaSecret) return res.status(503).json({ error: 'Alpaca credentials not configured' }); + const alpaca = await getUserAlpacaCredentials(authUserId); const now = new Date(); let start = new Date(now); @@ -2727,8 +2748,8 @@ RULES: } const r = await fetch(url, { headers: { - 'APCA-API-KEY-ID': alpacaKey, - 'APCA-API-SECRET-KEY': alpacaSecret, + 'APCA-API-KEY-ID': alpaca.key, + 'APCA-API-SECRET-KEY': alpaca.secret, }, }); if (!r.ok) { @@ -2754,6 +2775,9 @@ RULES: })); res.json({ symbol, period, bars }); } catch (error: any) { + if (error instanceof MissingServiceConfigError) { + return res.status(503).json({ error: error.message }); + } res.status(500).json({ error: error.message }); } }); @@ -2761,18 +2785,18 @@ RULES: // ── News: proxy to Alpaca /v1beta1/news ─────────────────────────────── this.app.get('/api/news', this.requireAuth, async (req, res) => { try { + const authUserId = (req as AuthenticatedRequest).authUserId; let symbols = ''; try { symbols = normalizeNewsSymbolsQuery(req.query.symbols); } catch (validationError: any) { return res.status(400).json({ error: validationError.message }); } - const limit = Math.max(1, Math.min(50, Number(req.query.limit) || 10)); - const alpacaKey = config.ALPACA_API_KEY; - const alpacaSecret = config.ALPACA_API_SECRET; - if (!alpacaKey || !alpacaSecret) { - return res.status(503).json({ error: 'Alpaca credentials not configured' }); + if (!authUserId) { + return res.status(401).json({ error: 'Unauthorized' }); } + const limit = Math.max(1, Math.min(50, Number(req.query.limit) || 10)); + const alpaca = await getUserAlpacaCredentials(authUserId); const qs = new URLSearchParams({ ...(symbols ? { symbols } : {}), limit: String(limit), @@ -2781,14 +2805,17 @@ RULES: const url = `https://data.alpaca.markets/v1beta1/news?${qs.toString()}`; const r = await fetch(url, { headers: { - 'APCA-API-KEY-ID': alpacaKey, - 'APCA-API-SECRET-KEY': alpacaSecret, + 'APCA-API-KEY-ID': alpaca.key, + 'APCA-API-SECRET-KEY': alpaca.secret, }, }); if (!r.ok) return res.status(r.status).json({ error: 'Alpaca news fetch failed' }); const data = await r.json() as any; res.json({ news: data.news ?? data }); } catch (error: any) { + if (error instanceof MissingServiceConfigError) { + return res.status(503).json({ error: error.message }); + } res.status(500).json({ error: error.message }); } }); @@ -2796,22 +2823,25 @@ RULES: // ── Market indices: SPY / DIA / QQQ snapshots from Alpaca ───────────── this.app.get('/api/market/indices', this.requireAuth, async (req, res) => { try { - const alpacaKey = config.ALPACA_API_KEY; - const alpacaSecret = config.ALPACA_API_SECRET; - if (!alpacaKey || !alpacaSecret) { - return res.status(503).json({ error: 'Alpaca credentials not configured' }); + const authUserId = (req as AuthenticatedRequest).authUserId; + if (!authUserId) { + return res.status(401).json({ error: 'Unauthorized' }); } + const alpaca = await getUserAlpacaCredentials(authUserId); const url = 'https://data.alpaca.markets/v2/stocks/snapshots?symbols=SPY,DIA,QQQ&feed=iex'; const r = await fetch(url, { headers: { - 'APCA-API-KEY-ID': alpacaKey, - 'APCA-API-SECRET-KEY': alpacaSecret, + 'APCA-API-KEY-ID': alpaca.key, + 'APCA-API-SECRET-KEY': alpaca.secret, }, }); if (!r.ok) return res.status(r.status).json({ error: 'Alpaca snapshots fetch failed' }); const data = await r.json() as any; res.json(data); } catch (error: any) { + if (error instanceof MissingServiceConfigError) { + return res.status(503).json({ error: error.message }); + } res.status(500).json({ error: error.message }); } });