Send the Supabase token the app already had
Auth is Supabase end to end and always was: the backend verifies access tokens asymmetrically against the project's published JWKS and holds no signing key of its own, and `requireUser` wants that token in `Authorization: Bearer`. Nothing about that needed changing. What was wrong is only here. The two REST calls forwarded `useRequestHeaders(["cookie"])`, which is what you do when a Nuxt server is proxying a browser's request - and this app sets `ssr: false` so the same static bundle can serve the browser and the Tauri shell. There is no server to forward anything, so the header arrived empty and every call should have been getting a flat 401. The token was to hand the whole time. `stores/telemetry.ts` has been reading `session.access_token` for its socket since it was written; the REST side simply never did. `useApiFetch` is now the one way this app calls the backend, and it reads the token inside the fetcher rather than capturing it at setup, because supabase-js refreshes the session in the background about once an hour - a captured token would work until it quietly stopped. With no session it omits the header entirely rather than sending an empty Bearer, which the backend rejects with a message about the header's shape and reads like a client bug; getting the user a session is the Supabase module's redirect's job. The socket keeps using a `?token=` query parameter, which is not an inconsistency: a browser WebSocket cannot set request headers. `nuxt typecheck` clean.
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* The one way this app calls `continuum-backend`.
|
||||
*
|
||||
* Auth is Supabase throughout: the backend verifies access tokens
|
||||
* asymmetrically against the project's published JWKS and holds no signing
|
||||
* key of its own (`continuum-backend/src/auth/verifier.ts`), and
|
||||
* `requireUser` wants that token in `Authorization: Bearer`. So this attaches
|
||||
* it, and that is the whole of the client's side of authentication.
|
||||
*
|
||||
* What this replaces: `useRequestHeaders(["cookie"])` on each call site.
|
||||
* Forwarding a cookie is what you do when a Nuxt *server* is proxying a
|
||||
* browser's request, and this app sets `ssr: false` so that it can ship the
|
||||
* same static bundle to the browser and to the Tauri shell - there is no
|
||||
* server to forward anything, so the header arrived empty and every call got
|
||||
* a 401. The token was already to hand the whole time; the telemetry store
|
||||
* has been reading it for its socket.
|
||||
*
|
||||
* The token is read inside the fetcher rather than captured when this is
|
||||
* called, because supabase-js refreshes the session in the background about
|
||||
* once an hour. A token captured at setup would work until it quietly
|
||||
* stopped.
|
||||
*/
|
||||
export function useApiFetch() {
|
||||
const config = useRuntimeConfig();
|
||||
const session = useSupabaseSession();
|
||||
|
||||
return <T>(path: string, options: Parameters<typeof $fetch>[1] = {}) => {
|
||||
const token = session.value?.access_token;
|
||||
|
||||
return $fetch<T>(`${config.public.apiBase}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
// Omitted rather than sent empty when there is no session. An empty
|
||||
// Bearer is a malformed credential the backend rejects with a
|
||||
// message about the header's shape, which reads like a client bug;
|
||||
// no header at all is the honest "not signed in". The Supabase
|
||||
// module's own redirect is what handles getting the user a session.
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...options?.headers,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -21,7 +21,8 @@ export interface PrinterStream {
|
||||
* in: the caller needs one of these per printer, discovered from a list that
|
||||
* itself arrives asynchronously, and Nuxt's data composables may only be
|
||||
* called while a component is setting up. A loop over a resolved list is not
|
||||
* that.
|
||||
* that. It takes the fetcher as an argument for the same reason - the caller
|
||||
* builds it in setup, where `useApiFetch` is valid.
|
||||
*
|
||||
* **Absence is the normal case, not an error.** Only Bambu's X1/H2 class
|
||||
* publishes a camera URL a gateway can restream; a P1 or A1 reports that it
|
||||
@@ -35,12 +36,11 @@ export interface PrinterStream {
|
||||
* for anyone else.
|
||||
*/
|
||||
export async function fetchPrinterStream(
|
||||
apiBase: string,
|
||||
apiFetch: ReturnType<typeof useApiFetch>,
|
||||
printerId: string,
|
||||
headers: Record<string, string>,
|
||||
): Promise<PrinterStream | null> {
|
||||
try {
|
||||
return await $fetch<PrinterStream>(`${apiBase}/printers/${printerId}/stream`, { headers });
|
||||
return await apiFetch<PrinterStream>(`/printers/${printerId}/stream`);
|
||||
} catch (err: unknown) {
|
||||
// A 404 means "this printer has no watchable camera", which is expected
|
||||
// often enough that treating it as a failure would put an error state on
|
||||
|
||||
@@ -16,21 +16,18 @@ interface Printer {
|
||||
}
|
||||
|
||||
const telemetry = useTelemetryStore();
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
const { data: printers } = await useFetch<Printer[]>(`${config.public.apiBase}/printers`, {
|
||||
headers: useRequestHeaders(["cookie"]),
|
||||
});
|
||||
const apiFetch = useApiFetch();
|
||||
|
||||
const { data: printers } = await useAsyncData("printers", () =>
|
||||
apiFetch<Printer[]>("/printers"),
|
||||
);
|
||||
|
||||
// Looked up per printer, because the answer is per printer, but only for the
|
||||
// printers the list already says have a camera. Asking for all of them would
|
||||
// mean one request per machine in the fleet, most of them 404 by design.
|
||||
const streams = ref<Record<string, PrinterStream | null>>({});
|
||||
|
||||
// Read here, in setup, rather than inside the loop below: Nuxt's request
|
||||
// helpers are only valid while a component is setting up.
|
||||
const streamHeaders = useRequestHeaders(["cookie"]) as Record<string, string>;
|
||||
|
||||
watch(
|
||||
printers,
|
||||
(list) => {
|
||||
@@ -41,7 +38,7 @@ watch(
|
||||
// no player, which is also the right thing to show while this is in
|
||||
// flight.
|
||||
streams.value[printer.id] = null;
|
||||
void fetchPrinterStream(config.public.apiBase, printer.id, streamHeaders).then((stream) => {
|
||||
void fetchPrinterStream(apiFetch, printer.id).then((stream) => {
|
||||
streams.value[printer.id] = stream;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user