Files
foxhunt/web-dashboard/src/lib/grpc.ts
jgrusewski 84877e5146 feat: add auth gRPC service + migrate dashboard from REST/WS to grpc-web
Backend (Rust):
- Create proto/auth.proto with Login and RefreshToken RPCs
- Implement AuthGrpcService in services/api with JWT issuance
- Register as unauthenticated service in main.rs (pre-auth)
- Update JWT issuer from foxhunt-api-gateway to foxhunt-api

Dashboard (TypeScript):
- Install @connectrpc/connect-web + @bufbuild/protobuf
- Generate TypeScript proto clients via buf (src/gen/)
- Create grpc.ts transport with JWT interceptor
- Rewrite all useApi hooks to typed gRPC calls
- Replace WebSocket with useGrpcStream server-streaming hooks
- Migrate all 6 pages + 6 components to grpc-web
- Delete api.ts, websocket.ts, useWebSocket.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:05:18 +01:00

81 lines
2.8 KiB
TypeScript

import { createGrpcWebTransport } from "@connectrpc/connect-web";
import { createClient, type Interceptor } from "@connectrpc/connect";
import {
getToken,
isTokenExpired,
clearTokens,
SESSION_EXPIRED_KEY,
} from "./auth";
// Generated service definitions (protoc-gen-es v2 — services live in *_pb.ts)
import { TradingService } from "../gen/trading_pb";
import { RiskService } from "../gen/risk_pb";
import { MLService } from "../gen/ml_pb";
import { MLTrainingService } from "../gen/ml_training_pb";
import { ConfigService } from "../gen/config_pb";
import { MonitoringService } from "../gen/monitoring_pb";
import { BacktestingService } from "../gen/fxt_trading_pb";
import { AuthService } from "../gen/auth_pb";
const API_URL = import.meta.env.VITE_API_URL ?? "http://localhost:50051";
/** Margin in seconds before actual expiry to treat the token as expired */
const EXPIRY_MARGIN_SECONDS = 60;
/**
* Interceptor that attaches the JWT Authorization header to every request
* and handles session expiry / UNAUTHENTICATED responses.
*/
const authInterceptor: Interceptor = (next) => async (req) => {
const token = getToken();
if (token) {
if (isTokenExpired(token, EXPIRY_MARGIN_SECONDS)) {
sessionStorage.setItem(SESSION_EXPIRED_KEY, "1");
clearTokens();
window.location.href = "/login";
throw new Error("Session expired");
}
req.header.set("authorization", `Bearer ${token}`);
}
try {
return await next(req);
} catch (err: unknown) {
// gRPC status 16 = UNAUTHENTICATED
if (
err &&
typeof err === "object" &&
"code" in err &&
(err as { code: number }).code === 16
) {
sessionStorage.setItem(SESSION_EXPIRED_KEY, "1");
clearTokens();
window.location.href = "/login";
}
throw err;
}
};
/** Authenticated transport — used for all services except login */
const transport = createGrpcWebTransport({
baseUrl: API_URL,
interceptors: [authInterceptor],
});
/** Public transport — no auth header, used for login/refresh */
const publicTransport = createGrpcWebTransport({
baseUrl: API_URL,
});
// ---------------------------------------------------------------------------
// Service clients
// ---------------------------------------------------------------------------
export const authClient = createClient(AuthService, publicTransport);
export const tradingClient = createClient(TradingService, transport);
export const riskClient = createClient(RiskService, transport);
export const mlClient = createClient(MLService, transport);
export const mlTrainingClient = createClient(MLTrainingService, transport);
export const configClient = createClient(ConfigService, transport);
export const monitoringClient = createClient(MonitoringService, transport);
export const backtestingClient = createClient(BacktestingService, transport);