feat(settings): add per-user fmp api key

This commit is contained in:
root 2026-05-05 23:08:31 +00:00
parent adfadd824b
commit 39456473cb
7 changed files with 85 additions and 5 deletions

View File

@ -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') {

View File

@ -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,

View File

@ -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;

View File

@ -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 || ''),

View File

@ -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;

View File

@ -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;

View File

@ -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>
);