drop/desktop/main/composables/api.ts
wdunn001 c7ffc2b994 desktop: route News through the client-JWT path; bump to 0.4.4
News called /api/v1/client/news through apiGet(), which sends the
webtoken Bearer obtained from POST /api/v1/client/user/webtoken. But
that route is a defineClientEventHandler -- it verifies the clients own
short-lived signed JWT and never consults the webtokens ACLs at all, so
it rejected the request outright. Store and Library have always worked
because they already use the JWT path.

Adds api_get_jwt on the Rust side and apiGetClient() in the composable,
and points News at it. Verified working against the live server.

Two auth mechanisms coexist here and picking the wrong one yields a
clean 403 that looks like a permissions problem: /api/v1/client/* wants
the client JWT, while routes gated by aclManager.getUserIdACL want the
webtoken plus the right ACL.
2026-08-03 23:46:04 -04:00

51 lines
2.1 KiB
TypeScript

import { invoke } from "@tauri-apps/api/core";
// Thin wrappers over the generic Rust bridge (community_api.rs): the client
// passes the server's own `/api/v1/...` path, Rust resolves the base URL +
// signs the request with the same auth every other client call uses. See
// desktop/src-tauri/src/community_api.rs for why this is generic rather than
// one typed Tauri command per endpoint.
//
// On a non-2xx response the Rust side rejects with a stringified
// RemoteAccessError::InvalidResponse whose message is the server's own
// `statusMessage`/`message` -- callers can show `(e as string)` directly.
export function apiGet<T = any>(
path: string,
query?: Record<string, string | number | boolean | undefined>,
): Promise<T> {
const q = query
? Object.entries(query)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, String(v)] as [string, string])
: undefined;
return invoke<T>("api_get", { path, query: q });
}
// Same as apiGet, but for `/api/v1/client/*` routes built on
// `defineClientEventHandler` (news, library, game manifests, ...). Those
// routes authenticate with the desktop client's own short-lived JWT
// (Rust's generate_authorization_header), NOT the aclManager webtoken
// apiGet's api_get command mints -- sending them a webtoken 403s no matter
// what ACLs it carries, since defineClientEventHandler never looks at
// ACLs at all. See desktop/src-tauri/src/community_api.rs's module header
// for the full explanation of why there are two auth paths here.
export function apiGetClient<T = any>(
path: string,
query?: Record<string, string | number | boolean | undefined>,
): Promise<T> {
const q = query
? Object.entries(query)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, String(v)] as [string, string])
: undefined;
return invoke<T>("api_get_jwt", { path, query: q });
}
export function apiPost<T = any>(path: string, body?: unknown): Promise<T> {
return invoke<T>("api_post", { path, body: body ?? {} });
}
export function apiDelete<T = any>(path: string): Promise<T> {
return invoke<T>("api_delete", { path });
}