arbitrage-engine/frontend/lib/auth.tsx
fanziqi ad60a53262 review: add code audit annotations and REVIEW.md for v5.1
P0 issues annotated (critical, must fix before live trading):
- signal_engine.py: cooldown blocks reverse-signal position close
- paper_monitor.py + signal_engine.py: pnl_r 2x inflated for TP scenarios
- signal_engine.py: entry price uses 30min VWAP instead of real-time price
- paper_monitor.py + signal_engine.py: concurrent write race on paper_trades

P1 issues annotated (long-term stability):
- db.py: ensure_partitions uses timedelta(30d) causing missed monthly partitions
- signal_engine.py: float precision drift in buy_vol/sell_vol accumulation
- market_data_collector.py: single bare connection with no reconnect logic
- db.py: get_sync_pool initialization not thread-safe
- signal_engine.py: recent_large_trades deque has no maxlen

P2/P3 issues annotated across backend and frontend:
- coinbase_premium KeyError for XRP/SOL symbols
- liquidation_collector: redundant elif condition in aggregation logic
- auth.py: JWT secret hardcoded default, login rate-limit absent
- Frontend: concurrent refresh token race, AuthContext not synced on failure
- Frontend: universal catch{} swallows all API errors silently
- Frontend: serial API requests in LatestSignals, market-indicators over-polling

docs/REVIEW.md: comprehensive audit report with all 34 issues (P0×4, P1×5,
P2×6, P3×4 backend + FE-P1×4, FE-P2×8, FE-P3×3 frontend), fix suggestions
and prioritized remediation roadmap.
2026-03-01 17:14:52 +08:00

154 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { createContext, useContext, useState, useEffect, ReactNode, useCallback } from "react";
interface User {
id: number;
email: string;
role: string;
}
interface AuthState {
user: User | null;
accessToken: string | null;
loading: boolean;
login: (email: string, password: string) => Promise<void>;
register: (email: string, password: string, inviteCode: string) => Promise<void>;
logout: () => void;
isLoggedIn: boolean;
isAdmin: boolean;
}
const AuthContext = createContext<AuthState | undefined>(undefined);
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "";
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [accessToken, setAccessToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
// init from localStorage
useEffect(() => {
// [REVIEW] FE-P3-1 | Tokens 存储在 localStorage存在 XSS 盗取风险
// 任何能在页面执行的 JS包括第三方依赖的供应链攻击都能读取 localStorage
// 对于交易系统建议改用 httpOnly Secure cookie需后端配合 /api/auth/session 端点)
const token = localStorage.getItem("access_token");
const saved = localStorage.getItem("user");
if (token && saved) {
try {
setAccessToken(token);
setUser(JSON.parse(saved));
} catch {}
}
setLoading(false);
}, []);
const saveAuth = (data: { access_token: string; refresh_token: string; user: User }) => {
localStorage.setItem("access_token", data.access_token);
localStorage.setItem("refresh_token", data.refresh_token);
localStorage.setItem("user", JSON.stringify(data.user));
setAccessToken(data.access_token);
setUser(data.user);
};
const login = useCallback(async (email: string, password: string) => {
const res = await fetch(`${API_BASE}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || "Login failed");
}
const data = await res.json();
saveAuth(data);
}, []);
const register = useCallback(async (email: string, password: string, inviteCode: string) => {
const res = await fetch(`${API_BASE}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password, invite_code: inviteCode }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || "Registration failed");
}
const data = await res.json();
saveAuth(data);
}, []);
const logout = useCallback(() => {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
localStorage.removeItem("user");
setAccessToken(null);
setUser(null);
}, []);
return (
<AuthContext.Provider value={{
user, accessToken, loading, login, register, logout,
isLoggedIn: !!user, isAdmin: user?.role === "admin",
}}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}
// Authenticated fetch helper
export async function authFetch(path: string, options: RequestInit = {}): Promise<Response> {
const token = localStorage.getItem("access_token");
const headers = new Headers(options.headers);
if (token) headers.set("Authorization", `Bearer ${token}`);
let res = await fetch(`${API_BASE}${path}`, { ...options, headers });
// try refresh on 401
if (res.status === 401) {
const refreshToken = localStorage.getItem("refresh_token");
if (refreshToken) {
// [REVIEW] FE-P1-1 | 并发刷新竞态:多个组件同时 401 时会并发调用此逻辑
// 后端 refresh token 是单次使用(用完即 revoke多个并发刷新请求中只有第一个成功
// 其余请求的刷新会失败,进入 else 分支清除 localStorage导致部分组件数据停止更新
// 修复:用模块级 Promise 单例防止并发刷新
// let _refreshPromise: Promise<string|null> | null = null;
// if (!_refreshPromise) { _refreshPromise = doRefresh().finally(() => _refreshPromise = null); }
// const newToken = await _refreshPromise;
const refreshRes = await fetch(`${API_BASE}/api/auth/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh_token: refreshToken }),
});
if (refreshRes.ok) {
const data = await refreshRes.json();
localStorage.setItem("access_token", data.access_token);
localStorage.setItem("refresh_token", data.refresh_token);
headers.set("Authorization", `Bearer ${data.access_token}`);
res = await fetch(`${API_BASE}${path}`, { ...options, headers });
} else {
// [REVIEW] FE-P1-2 | 刷新失败后 React AuthContext 状态未同步
// 这里清除了 localStorage 但没有调用 AuthContext.logout()(无法直接访问 Context
// 结果React state 仍显示 isLoggedIn=trueuser 对象仍存在
// 用户 UI 看起来仍是登录状态,但所有后续 API 调用都会无声失败catch{} 吞掉错误)
// 修复方案A抛出特定错误码调用方 useEffect 中捕获并执行 logout()
// 修复方案B用 window.dispatchEvent(new CustomEvent("auth:logout")) 触发全局登出
// refresh failed, clear auth
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
localStorage.removeItem("user");
}
}
}
return res;
}