first commit
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import apiClient from './client';
|
||||
import type { AdminUser, SystemSetting } from '../lib/types';
|
||||
|
||||
// Users
|
||||
export function listUsers() {
|
||||
return apiClient.get<AdminUser[]>('admin/users').then((r) => r.data);
|
||||
}
|
||||
|
||||
export function createUser(data: {
|
||||
username: string;
|
||||
password: string;
|
||||
role: string;
|
||||
has_access: boolean;
|
||||
}) {
|
||||
return apiClient.post<AdminUser>('admin/users', data).then((r) => r.data);
|
||||
}
|
||||
|
||||
export function updateAccess(userId: number, hasAccess: boolean) {
|
||||
return apiClient
|
||||
.put<{ message: string }>(`admin/users/${userId}/access`, {
|
||||
has_access: hasAccess,
|
||||
})
|
||||
.then((r) => r.data);
|
||||
}
|
||||
|
||||
export function resetPassword(userId: number, password: string) {
|
||||
return apiClient
|
||||
.put<{ message: string }>(`admin/users/${userId}/password`, { password })
|
||||
.then((r) => r.data);
|
||||
}
|
||||
|
||||
// Settings
|
||||
export function listSettings() {
|
||||
return apiClient
|
||||
.get<SystemSetting[]>('admin/settings')
|
||||
.then((r) => r.data);
|
||||
}
|
||||
|
||||
export function updateSetting(key: string, value: string) {
|
||||
return apiClient
|
||||
.put<{ message: string }>(`admin/settings/${key}`, { value })
|
||||
.then((r) => r.data);
|
||||
}
|
||||
|
||||
export function updateRegistration(enabled: boolean) {
|
||||
return apiClient
|
||||
.put<{ message: string }>('admin/settings/registration', { enabled })
|
||||
.then((r) => r.data);
|
||||
}
|
||||
|
||||
// Jobs
|
||||
export interface JobStatus {
|
||||
name: string;
|
||||
label: string;
|
||||
enabled: boolean;
|
||||
next_run_at: string | null;
|
||||
registered: boolean;
|
||||
}
|
||||
|
||||
export function listJobs() {
|
||||
return apiClient.get<JobStatus[]>('admin/jobs').then((r) => r.data);
|
||||
}
|
||||
|
||||
export function toggleJob(jobName: string, enabled: boolean) {
|
||||
return apiClient
|
||||
.put<{ message: string }>(`admin/jobs/${jobName}/toggle`, { enabled })
|
||||
.then((r) => r.data);
|
||||
}
|
||||
|
||||
export function triggerJob(jobName: string) {
|
||||
return apiClient
|
||||
.post<{ message: string }>(`admin/jobs/${jobName}/trigger`)
|
||||
.then((r) => r.data);
|
||||
}
|
||||
|
||||
// Data cleanup
|
||||
export function cleanupData(olderThanDays: number) {
|
||||
return apiClient
|
||||
.post<{ message: string }>('admin/data/cleanup', {
|
||||
older_than_days: olderThanDays,
|
||||
})
|
||||
.then((r) => r.data);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import apiClient from './client';
|
||||
import type { TokenResponse } from '../lib/types';
|
||||
|
||||
export function login(username: string, password: string) {
|
||||
return apiClient
|
||||
.post<TokenResponse>('auth/login', { username, password })
|
||||
.then((r) => r.data);
|
||||
}
|
||||
|
||||
export function register(username: string, password: string) {
|
||||
return apiClient
|
||||
.post<{ message: string }>('auth/register', { username, password })
|
||||
.then((r) => r.data);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import axios from 'axios';
|
||||
import type { APIEnvelope } from '../lib/types';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
|
||||
/**
|
||||
* Typed error class for API errors, providing structured error handling
|
||||
* across the application.
|
||||
*/
|
||||
export class ApiError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Central Axios instance configured for the Stock Data Backend API.
|
||||
* - Base URL: /api/v1/
|
||||
* - Timeout: 30 seconds
|
||||
* - JSON content type
|
||||
*/
|
||||
const apiClient = axios.create({
|
||||
baseURL: '/api/v1/',
|
||||
timeout: 30_000,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
/**
|
||||
* Request interceptor: attaches JWT Bearer token from the auth store
|
||||
* to every outgoing request when a token is available.
|
||||
*/
|
||||
apiClient.interceptors.request.use((config) => {
|
||||
const token = useAuthStore.getState().token;
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
/**
|
||||
* Response interceptor:
|
||||
* - Success path: unwraps the { status, data, error } envelope, returning
|
||||
* only the `data` field. Throws ApiError if envelope status is 'error'.
|
||||
* - Error path: handles 401 by clearing auth and redirecting to login.
|
||||
* All other errors are wrapped in ApiError with a descriptive message.
|
||||
*/
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
const envelope = response.data as APIEnvelope;
|
||||
if (envelope.status === 'error') {
|
||||
throw new ApiError(envelope.error ?? 'Unknown API error');
|
||||
}
|
||||
// Return unwrapped data — callers receive the inner payload directly.
|
||||
// We override the response shape here; downstream API functions cast as needed.
|
||||
response.data = envelope.data;
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||
useAuthStore.getState().logout();
|
||||
window.location.href = '/login';
|
||||
}
|
||||
const msg =
|
||||
error.response?.data?.error ?? error.message ?? 'Network error';
|
||||
throw new ApiError(msg);
|
||||
},
|
||||
);
|
||||
|
||||
export default apiClient;
|
||||
@@ -0,0 +1,8 @@
|
||||
import apiClient from './client';
|
||||
import type { FundamentalResponse } from '../lib/types';
|
||||
|
||||
export function getFundamentals(symbol: string) {
|
||||
return apiClient
|
||||
.get<FundamentalResponse>(`fundamentals/${symbol}`)
|
||||
.then((r) => r.data);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import apiClient from './client';
|
||||
|
||||
export function check() {
|
||||
return apiClient.get<{ status: string }>('health').then((r) => r.data);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import apiClient from './client';
|
||||
import type { IndicatorResult, EMACrossResult } from '../lib/types';
|
||||
|
||||
interface IndicatorEnvelopeData {
|
||||
symbol: string;
|
||||
indicator: IndicatorResult;
|
||||
}
|
||||
|
||||
interface EMACrossEnvelopeData {
|
||||
symbol: string;
|
||||
ema_cross: EMACrossResult;
|
||||
}
|
||||
|
||||
export function getIndicator(symbol: string, indicatorType: string) {
|
||||
return apiClient
|
||||
.get<IndicatorEnvelopeData>(`indicators/${symbol}/${indicatorType}`)
|
||||
.then((r) => (r.data as unknown as IndicatorEnvelopeData).indicator);
|
||||
}
|
||||
|
||||
export function getEMACross(symbol: string) {
|
||||
return apiClient
|
||||
.get<EMACrossEnvelopeData>(`indicators/${symbol}/ema-cross`)
|
||||
.then((r) => (r.data as unknown as EMACrossEnvelopeData).ema_cross);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import apiClient from './client';
|
||||
|
||||
export function fetchData(symbol: string) {
|
||||
return apiClient
|
||||
.post<{ message: string }>(`ingestion/fetch/${symbol}`)
|
||||
.then((r) => r.data);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import apiClient from './client';
|
||||
import type { OHLCVBar } from '../lib/types';
|
||||
|
||||
export function getOHLCV(symbol: string) {
|
||||
return apiClient.get<OHLCVBar[]>(`ohlcv/${symbol}`).then((r) => r.data);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import apiClient from './client';
|
||||
import type { ScoreResponse, RankingsResponse } from '../lib/types';
|
||||
|
||||
export function getScores(symbol: string) {
|
||||
return apiClient
|
||||
.get<ScoreResponse>(`scores/${symbol}`)
|
||||
.then((r) => r.data);
|
||||
}
|
||||
|
||||
export function getRankings() {
|
||||
return apiClient.get<RankingsResponse>('rankings').then((r) => r.data);
|
||||
}
|
||||
|
||||
export function updateWeights(weights: Record<string, number>) {
|
||||
return apiClient
|
||||
.put<{ message: string }>('scores/weights', weights)
|
||||
.then((r) => r.data);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import apiClient from './client';
|
||||
import type { SentimentResponse } from '../lib/types';
|
||||
|
||||
export function getSentiment(symbol: string) {
|
||||
return apiClient
|
||||
.get<SentimentResponse>(`sentiment/${symbol}`)
|
||||
.then((r) => r.data);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import apiClient from './client';
|
||||
import type { SRLevelResponse } from '../lib/types';
|
||||
|
||||
export function getLevels(symbol: string) {
|
||||
return apiClient
|
||||
.get<SRLevelResponse>(`sr-levels/${symbol}`)
|
||||
.then((r) => r.data);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import apiClient from './client';
|
||||
import type { Ticker } from '../lib/types';
|
||||
|
||||
export function list() {
|
||||
return apiClient.get<Ticker[]>('tickers').then((r) => r.data);
|
||||
}
|
||||
|
||||
export function create(symbol: string) {
|
||||
return apiClient.post<Ticker>('tickers', { symbol }).then((r) => r.data);
|
||||
}
|
||||
|
||||
export function deleteTicker(symbol: string) {
|
||||
return apiClient
|
||||
.delete<{ message: string }>(`tickers/${symbol}`)
|
||||
.then((r) => r.data);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import apiClient from './client';
|
||||
import type { TradeSetup } from '../lib/types';
|
||||
|
||||
export function list() {
|
||||
return apiClient.get<TradeSetup[]>('trades').then((r) => r.data);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import apiClient from './client';
|
||||
import type { WatchlistEntry } from '../lib/types';
|
||||
|
||||
export function list() {
|
||||
return apiClient.get<WatchlistEntry[]>('watchlist').then((r) => r.data);
|
||||
}
|
||||
|
||||
export function add(symbol: string) {
|
||||
return apiClient
|
||||
.post<WatchlistEntry>(`watchlist/${symbol}`)
|
||||
.then((r) => r.data);
|
||||
}
|
||||
|
||||
export function remove(symbol: string) {
|
||||
return apiClient
|
||||
.delete<{ message: string }>(`watchlist/${symbol}`)
|
||||
.then((r) => r.data);
|
||||
}
|
||||
Reference in New Issue
Block a user