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.
154 lines
5.8 KiB
TypeScript
154 lines
5.8 KiB
TypeScript
"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=true,user 对象仍存在
|
||
// 用户 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;
|
||
}
|