fix(api): use user alpaca keys for market data

This commit is contained in:
root 2026-05-05 22:40:06 +00:00
parent 3867b6b296
commit 351412423f

View File

@ -146,6 +146,27 @@ class MissingServiceConfigError extends Error {
} }
} }
interface AlpacaCredentials {
key: string;
secret: string;
}
async function getUserAlpacaCredentials(userId: string): Promise<AlpacaCredentials> {
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 { interface TradeAuditEvent {
event: string; event: string;
userId?: string; userId?: string;
@ -2683,12 +2704,12 @@ RULES:
// ── Chart bars: Alpaca stock bars with period mapping ──────────────── // ── Chart bars: Alpaca stock bars with period mapping ────────────────
this.app.get('/api/chart/bars', this.requireAuth, async (req, res) => { this.app.get('/api/chart/bars', this.requireAuth, async (req, res) => {
try { try {
const authUserId = (req as AuthenticatedRequest).authUserId;
const symbol = String(req.query.symbol || '').trim().toUpperCase(); const symbol = String(req.query.symbol || '').trim().toUpperCase();
const period = String(req.query.period || '1Y').trim(); const period = String(req.query.period || '1Y').trim();
const alpacaKey = config.ALPACA_API_KEY; if (!authUserId) return res.status(401).json({ error: 'Unauthorized' });
const alpacaSecret = config.ALPACA_API_SECRET;
if (!symbol) return res.status(400).json({ error: 'symbol required' }); 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(); const now = new Date();
let start = new Date(now); let start = new Date(now);
@ -2727,8 +2748,8 @@ RULES:
} }
const r = await fetch(url, { const r = await fetch(url, {
headers: { headers: {
'APCA-API-KEY-ID': alpacaKey, 'APCA-API-KEY-ID': alpaca.key,
'APCA-API-SECRET-KEY': alpacaSecret, 'APCA-API-SECRET-KEY': alpaca.secret,
}, },
}); });
if (!r.ok) { if (!r.ok) {
@ -2754,6 +2775,9 @@ RULES:
})); }));
res.json({ symbol, period, bars }); res.json({ symbol, period, bars });
} catch (error: any) { } catch (error: any) {
if (error instanceof MissingServiceConfigError) {
return res.status(503).json({ error: error.message });
}
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
}); });
@ -2761,18 +2785,18 @@ RULES:
// ── News: proxy to Alpaca /v1beta1/news ─────────────────────────────── // ── News: proxy to Alpaca /v1beta1/news ───────────────────────────────
this.app.get('/api/news', this.requireAuth, async (req, res) => { this.app.get('/api/news', this.requireAuth, async (req, res) => {
try { try {
const authUserId = (req as AuthenticatedRequest).authUserId;
let symbols = ''; let symbols = '';
try { try {
symbols = normalizeNewsSymbolsQuery(req.query.symbols); symbols = normalizeNewsSymbolsQuery(req.query.symbols);
} catch (validationError: any) { } catch (validationError: any) {
return res.status(400).json({ error: validationError.message }); return res.status(400).json({ error: validationError.message });
} }
const limit = Math.max(1, Math.min(50, Number(req.query.limit) || 10)); if (!authUserId) {
const alpacaKey = config.ALPACA_API_KEY; return res.status(401).json({ error: 'Unauthorized' });
const alpacaSecret = config.ALPACA_API_SECRET;
if (!alpacaKey || !alpacaSecret) {
return res.status(503).json({ error: 'Alpaca credentials not configured' });
} }
const limit = Math.max(1, Math.min(50, Number(req.query.limit) || 10));
const alpaca = await getUserAlpacaCredentials(authUserId);
const qs = new URLSearchParams({ const qs = new URLSearchParams({
...(symbols ? { symbols } : {}), ...(symbols ? { symbols } : {}),
limit: String(limit), limit: String(limit),
@ -2781,14 +2805,17 @@ RULES:
const url = `https://data.alpaca.markets/v1beta1/news?${qs.toString()}`; const url = `https://data.alpaca.markets/v1beta1/news?${qs.toString()}`;
const r = await fetch(url, { const r = await fetch(url, {
headers: { headers: {
'APCA-API-KEY-ID': alpacaKey, 'APCA-API-KEY-ID': alpaca.key,
'APCA-API-SECRET-KEY': alpacaSecret, 'APCA-API-SECRET-KEY': alpaca.secret,
}, },
}); });
if (!r.ok) return res.status(r.status).json({ error: 'Alpaca news fetch failed' }); if (!r.ok) return res.status(r.status).json({ error: 'Alpaca news fetch failed' });
const data = await r.json() as any; const data = await r.json() as any;
res.json({ news: data.news ?? data }); res.json({ news: data.news ?? data });
} catch (error: any) { } catch (error: any) {
if (error instanceof MissingServiceConfigError) {
return res.status(503).json({ error: error.message });
}
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
}); });
@ -2796,22 +2823,25 @@ RULES:
// ── Market indices: SPY / DIA / QQQ snapshots from Alpaca ───────────── // ── Market indices: SPY / DIA / QQQ snapshots from Alpaca ─────────────
this.app.get('/api/market/indices', this.requireAuth, async (req, res) => { this.app.get('/api/market/indices', this.requireAuth, async (req, res) => {
try { try {
const alpacaKey = config.ALPACA_API_KEY; const authUserId = (req as AuthenticatedRequest).authUserId;
const alpacaSecret = config.ALPACA_API_SECRET; if (!authUserId) {
if (!alpacaKey || !alpacaSecret) { return res.status(401).json({ error: 'Unauthorized' });
return res.status(503).json({ error: 'Alpaca credentials not configured' });
} }
const alpaca = await getUserAlpacaCredentials(authUserId);
const url = 'https://data.alpaca.markets/v2/stocks/snapshots?symbols=SPY,DIA,QQQ&feed=iex'; const url = 'https://data.alpaca.markets/v2/stocks/snapshots?symbols=SPY,DIA,QQQ&feed=iex';
const r = await fetch(url, { const r = await fetch(url, {
headers: { headers: {
'APCA-API-KEY-ID': alpacaKey, 'APCA-API-KEY-ID': alpaca.key,
'APCA-API-SECRET-KEY': alpacaSecret, 'APCA-API-SECRET-KEY': alpaca.secret,
}, },
}); });
if (!r.ok) return res.status(r.status).json({ error: 'Alpaca snapshots fetch failed' }); if (!r.ok) return res.status(r.status).json({ error: 'Alpaca snapshots fetch failed' });
const data = await r.json() as any; const data = await r.json() as any;
res.json(data); res.json(data);
} catch (error: any) { } catch (error: any) {
if (error instanceof MissingServiceConfigError) {
return res.status(503).json({ error: error.message });
}
res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
} }
}); });