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.
74 lines
2.1 KiB
Vue
74 lines
2.1 KiB
Vue
<script setup lang="ts">
|
|
import { PrinterCard } from "@continuum/ui";
|
|
import { useTelemetryStore } from "~/stores/telemetry";
|
|
|
|
interface Printer {
|
|
id: string;
|
|
name: string;
|
|
model: string;
|
|
farmId: string;
|
|
/**
|
|
* Set only for a printer whose camera the gateway can actually restream,
|
|
* which in a mixed fleet is the minority. Its presence is what decides
|
|
* whether to ask for a stream URL at all - see the lookup below.
|
|
*/
|
|
cameraStream?: string | null;
|
|
}
|
|
|
|
const telemetry = useTelemetryStore();
|
|
|
|
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>>({});
|
|
|
|
watch(
|
|
printers,
|
|
(list) => {
|
|
for (const printer of list ?? []) {
|
|
if (!printer.cameraStream || printer.id in streams.value) continue;
|
|
// Marked before the request resolves, so a re-render in the meantime
|
|
// does not fire a second lookup for the same printer. `null` renders
|
|
// no player, which is also the right thing to show while this is in
|
|
// flight.
|
|
streams.value[printer.id] = null;
|
|
void fetchPrinterStream(apiFetch, printer.id).then((stream) => {
|
|
streams.value[printer.id] = stream;
|
|
});
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="fleet">
|
|
<h1>Fleet overview</h1>
|
|
<div class="fleet__grid">
|
|
<PrinterCard
|
|
v-for="printer in printers ?? []"
|
|
:key="printer.id"
|
|
:printer="printer"
|
|
:live="telemetry.printers[printer.id]"
|
|
:stream="streams[printer.id]"
|
|
/>
|
|
</div>
|
|
<p v-if="!printers?.length">No printers registered yet.</p>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.fleet__grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
gap: 1rem;
|
|
margin-top: 1rem;
|
|
}
|
|
</style>
|