d16456cb92
# 사용자별 격리 - JWT 토큰에 uid 추가 (auth.get_uid 헬퍼) - PostgreSQL — exchange_credentials/automation_config/trades/signal_events 에 user_id BIGINT - SQLite user_settings 테이블 신설 (글로벌 settings 는 옛 호환) - 모든 DB 함수 시그니처에 user_id 인자 추가 — 다른 사용자 데이터 절대 접근 불가 - alert_state — 모든 dict key 가 (user_id, ...) tuple 로 계층화 - core_logic alert_loop — 활성 사용자 순회 + 각자 settings/symbol/텔레그램 적용 - ensure_user_defaults() / ensure_user_automation() — 첫 사용 시 자동 시드 # 사용자 관리 (admin only) - users_db: delete_user / admin_reset_password / set_role - /api/users POST DELETE PUT password PUT role (본인 강등 / 마지막 admin 보호) - /admin/users 페이지 — 등록/삭제/role 토글/비번 reset 모달 - 사이드바 adminOnly 필터 — admin role 만 메뉴 노출 # 대시보드 개선 - 모바일 / 범례 토글 (모바일 60 캔들, 데스크톱 200) - 트레이드 이력: open 트레이드 실시간 PnL% (Binance ticker 호출 + 방향별 계산) - 메트릭 카드 분리 (실거래 vs 실시간 open) # 안정성 - api.ts: error.detail array/object 안전 처리 ([object Object] 방지) - Chart.tsx: Plotly yaxis title 객체 형태 + 모바일 height 동적 조정 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
50 lines
1.9 KiB
TypeScript
50 lines
1.9 KiB
TypeScript
// 클라이언트 API wrapper. JWT 는 localStorage 에 보관.
|
|
const TOKEN_KEY = 'jm_token';
|
|
|
|
export function getToken(): string | null {
|
|
if (typeof window === 'undefined') return null;
|
|
return localStorage.getItem(TOKEN_KEY);
|
|
}
|
|
export function setToken(t: string) { localStorage.setItem(TOKEN_KEY, t); }
|
|
export function clearToken() { localStorage.removeItem(TOKEN_KEY); }
|
|
|
|
async function request<T = any>(path: string, opts: RequestInit = {}): Promise<T> {
|
|
const token = getToken();
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(opts.headers as any),
|
|
};
|
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
|
const res = await fetch(path, { ...opts, headers });
|
|
if (res.status === 401) {
|
|
clearToken();
|
|
if (typeof window !== 'undefined' && window.location.pathname !== '/login') {
|
|
window.location.href = '/login';
|
|
}
|
|
throw new Error('unauthorized');
|
|
}
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
let msg = text;
|
|
try {
|
|
const j = JSON.parse(text);
|
|
const d = j.detail;
|
|
if (typeof d === 'string') msg = d;
|
|
else if (Array.isArray(d)) msg = d.map((e: any) => e?.msg || e?.detail || JSON.stringify(e)).join(', ');
|
|
else if (d && typeof d === 'object') msg = d.msg || JSON.stringify(d);
|
|
else msg = text;
|
|
} catch {}
|
|
if (typeof msg !== 'string') msg = String(msg);
|
|
throw new Error(msg || `${res.status}`);
|
|
}
|
|
if (res.status === 204) return undefined as any;
|
|
return res.json();
|
|
}
|
|
|
|
export const api = {
|
|
get: <T = any>(p: string) => request<T>(p),
|
|
post: <T = any>(p: string, body: any) => request<T>(p, { method: 'POST', body: JSON.stringify(body) }),
|
|
put: <T = any>(p: string, body: any) => request<T>(p, { method: 'PUT', body: JSON.stringify(body) }),
|
|
delete: <T = any>(p: string) => request<T>(p, { method: 'DELETE' }),
|
|
};
|