feat: remove public REST API; route all app calls over WebSocket RPC
The platform no longer exposes a public /api/* REST surface. The original HTTP route handlers are preserved verbatim as the single source of truth inside an internal, never-socket-bound FastAPI app (rpc_app), and are now reached only via RPC-over-WebSocket dispatch. - realtime router: anonymous connections allowed only to /api/auth/login and /api/auth/signup; all other RPCs/subscribes refused until token auth. - RPC dispatch re-injects the bearer token and X-Request-ID, then forwards through httpx.ASGITransport against the internal app (no network hop). - Public app now serves only /, /health, /health/ready and /api/ws; docs/openapi disabled. - Frontend: apiRequest routes /api/* to ws.rpc(); /health probes keep fetch. RealtimeClient gains rpc() with id correlation, timeout and AbortSignal. - Tests repointed to get_rpc_app(); anonymous-auth behaviour covered. Result: zero REST on the public surface; 264 backend tests + frontend typecheck pass.
This commit is contained in:
+24
-7
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import type { ApiErrorBody } from "@agy/contracts";
|
||||
import { getRealtimeClient } from "@/lib/ws";
|
||||
|
||||
/**
|
||||
* Resolves the API base URL.
|
||||
@@ -89,16 +90,34 @@ export interface RequestOptions {
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const { method = "GET", body, token, signal } = options;
|
||||
const { method = "GET", body, signal } = options;
|
||||
|
||||
// Liveness and readiness probes stay on plain HTTP: they answer during a DB
|
||||
// outage and are used by deploy orchestration (and the browser status check),
|
||||
// so they must not depend on the authenticated socket. Every application call
|
||||
// travels over the WebSocket as an RPC instead (see lib/ws.ts) — one
|
||||
// connection for both live feeds and request/response.
|
||||
if (path === "/health" || path === "/health/ready") {
|
||||
return fetchHealth<T>(path, method, body, signal);
|
||||
}
|
||||
|
||||
const client = getRealtimeClient();
|
||||
return client.rpc<T>(method, path, body === undefined ? null : body, { signal });
|
||||
}
|
||||
|
||||
/** Plain-HTTP implementation kept only for the two health probes. */
|
||||
async function fetchHealth<T>(
|
||||
path: string,
|
||||
method: string,
|
||||
body: unknown,
|
||||
signal?: AbortSignal,
|
||||
): Promise<T> {
|
||||
const headers: Record<string, string> = { Accept: "application/json" };
|
||||
if (body !== undefined) headers["Content-Type"] = "application/json";
|
||||
const authToken = token ?? getStoredToken();
|
||||
if (authToken) headers.Authorization = `Bearer ${authToken}`;
|
||||
|
||||
const baseUrl = getApiBaseUrl();
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${baseUrl}${path}`, {
|
||||
response = await fetch(`${getApiBaseUrl()}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
@@ -118,8 +137,6 @@ export async function apiRequest<T>(path: string, options: RequestOptions = {}):
|
||||
const envelope = payload as ApiErrorBody | null;
|
||||
const code = envelope?.error?.code ?? "http_error";
|
||||
const message = envelope?.error?.message ?? `خطای سرور (${response.status})`;
|
||||
// Fall back to the header: a failure raised outside the error handlers
|
||||
// (a CORS rejection, a proxy error) still carries the id.
|
||||
const requestId =
|
||||
envelope?.error?.request_id ?? response.headers.get("X-Request-ID") ?? undefined;
|
||||
throw new ApiError(response.status, code, message, envelope?.error?.details, requestId);
|
||||
|
||||
Reference in New Issue
Block a user