feat(settings): add per-user fmp api key
This commit is contained in:
parent
adfadd824b
commit
39456473cb
@ -139,6 +139,16 @@ const getConfiguredFmpApiKey = (): string => {
|
||||
return apiKey;
|
||||
};
|
||||
|
||||
async function getUserFmpApiKey(userId: string): Promise<string> {
|
||||
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') {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -27,6 +27,7 @@ function normalizeUser(row: Partial<UserConfig> | 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 || ''),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) => {
|
||||
</div>
|
||||
|
||||
{/* Break */}
|
||||
<div className="form-group full-width" style={{ marginTop: '10px', opacity: 0.5 }}>
|
||||
<hr style={{ border: '0', borderTop: '1px solid rgba(255,255,255,0.1)' }} />
|
||||
<small>Research & Screener Credentials</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group full-width">
|
||||
<label>Financial Modeling Prep API Key</label>
|
||||
<input
|
||||
name="FMP_API_KEY"
|
||||
value={formData.FMP_API_KEY}
|
||||
onChange={handleChange}
|
||||
disabled={!editing}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group full-width">
|
||||
<div className="settings-hint">
|
||||
<span>
|
||||
Used for screener, company profile, key metrics, and earnings data.
|
||||
</span>
|
||||
<a
|
||||
href="https://site.financialmodelingprep.com/developer/docs"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Get your Financial Modeling Prep API key
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group full-width" style={{ marginTop: '10px', opacity: 0.5 }}>
|
||||
<hr style={{ border: '0', borderTop: '1px solid rgba(255,255,255,0.1)' }} />
|
||||
<small>Alpaca Credentials</small>
|
||||
@ -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;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user