fix(api): use user alpaca keys for market data
This commit is contained in:
parent
3867b6b296
commit
351412423f
@ -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 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user