From 82379fa3e2f71dd71548b38675288b554f891e4e Mon Sep 17 00:00:00 2001 From: Quick104 <31828688+Quick104@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:51:43 -0400 Subject: [PATCH 1/2] fix(web): make Add Section work on insecure (plain-HTTP) origins Both add-section paths on Settings > Home Screen called crypto.randomUUID() unguarded. Browsers only expose randomUUID in secure contexts, so on self-hosted servers accessed over plain HTTP the click handler threw synchronously and the Add section button appeared dead while Cancel still worked. api/client.ts and plexAuth.ts already carried ad-hoc fallbacks for the same problem; extract a shared lib/uuid helper (UUIDv4 via crypto.getRandomValues, available in insecure contexts) and use it at all four call sites. Co-Authored-By: Claude Fable 5 --- web/src/api/client.ts | 22 ++++------------- .../sections/SectionEditorDrawer.tsx | 3 ++- web/src/lib/plexAuth.ts | 7 +++--- web/src/lib/uuid.ts | 24 +++++++++++++++++++ web/src/pages/settings/HomeScreenSettings.tsx | 3 ++- 5 files changed, 35 insertions(+), 24 deletions(-) create mode 100644 web/src/lib/uuid.ts diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 77f93cf6..6a66932b 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -1,5 +1,6 @@ import type { ApiError, RefreshResponse } from "./types"; import { storage } from "../utils/storage"; +import { randomUUID } from "../lib/uuid"; type ProfileUnverifiedListener = () => void; let profileUnverifiedListener: ProfileUnverifiedListener | null = null; @@ -82,25 +83,14 @@ export function getProfileToken(): string | null { return profileToken; } -function getOrCreateDeviceId(): string | null { +function getOrCreateDeviceId(): string { const existing = storage.get(storage.KEYS.DEVICE_ID); if (existing) { return existing; } - let nextId: string | null = null; - try { - nextId = - typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" - ? crypto.randomUUID() - : `web-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; - } catch { - nextId = `web-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; - } - - if (nextId) { - storage.set(storage.KEYS.DEVICE_ID, nextId); - } + const nextId = randomUUID(); + storage.set(storage.KEYS.DEVICE_ID, nextId); return nextId; } @@ -137,10 +127,6 @@ function detectDeviceName(): string { function getDeviceHeaders(): Record { const deviceId = getOrCreateDeviceId(); - if (!deviceId) { - return {}; - } - return { "X-Silo-Device-Id": deviceId, "X-Silo-Device-Name": detectDeviceName(), diff --git a/web/src/components/sections/SectionEditorDrawer.tsx b/web/src/components/sections/SectionEditorDrawer.tsx index 50a39eab..162db83d 100644 --- a/web/src/components/sections/SectionEditorDrawer.tsx +++ b/web/src/components/sections/SectionEditorDrawer.tsx @@ -38,6 +38,7 @@ import { useAllUserCollections, type CollectionOption, } from "@/hooks/queries/useAllUserCollections"; +import { randomUUID } from "@/lib/uuid"; const CATEGORY_LABELS: Record = { library_staples: "Library", @@ -142,7 +143,7 @@ export function buildProfileSectionSaveEntry({ } return { - id: section?.id ?? crypto.randomUUID(), + id: section?.id ?? randomUUID(), section_type: sectionType, title: title || sectionTypeLabel(sectionType), featured, diff --git a/web/src/lib/plexAuth.ts b/web/src/lib/plexAuth.ts index a8e32781..ae4d5bef 100644 --- a/web/src/lib/plexAuth.ts +++ b/web/src/lib/plexAuth.ts @@ -1,3 +1,5 @@ +import { randomUUID } from "./uuid"; + const PLEX_TV_BASE_URL = "https://plex.tv"; const PLEX_AUTH_BASE_URL = "https://app.plex.tv/auth#?"; const PLEX_PRODUCT = "Silo"; @@ -63,10 +65,7 @@ export function getPlexClientIdentifier(): string { return stored; } - const generated = - typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" - ? crypto.randomUUID() - : `silo-${Date.now()}`; + const generated = randomUUID(); setStoredPlexClientIdentifier(generated); return generated; } diff --git a/web/src/lib/uuid.ts b/web/src/lib/uuid.ts new file mode 100644 index 00000000..fba9a5e3 --- /dev/null +++ b/web/src/lib/uuid.ts @@ -0,0 +1,24 @@ +/** + * Generates an RFC 4122 version 4 UUID. + * + * `crypto.randomUUID` is only exposed in secure contexts, so self-hosted + * deployments served over plain HTTP need the `crypto.getRandomValues` + * fallback, which is available everywhere. + */ +export function randomUUID(): string { + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + const bytes = new Uint8Array(16); + if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") { + crypto.getRandomValues(bytes); + } else { + for (let i = 0; i < bytes.length; i += 1) { + bytes[i] = Math.floor(Math.random() * 256); + } + } + bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40; + bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80; + const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} diff --git a/web/src/pages/settings/HomeScreenSettings.tsx b/web/src/pages/settings/HomeScreenSettings.tsx index 95556a0e..3cca6735 100644 --- a/web/src/pages/settings/HomeScreenSettings.tsx +++ b/web/src/pages/settings/HomeScreenSettings.tsx @@ -27,6 +27,7 @@ import RecipeConfigDrawer from "@/components/RecipeGallery/RecipeConfigDrawer"; import type { AddPayload } from "@/components/RecipeGallery/RecipeConfigDrawer"; import type { GalleryPreset, RecipeDefinition } from "@/lib/recipes"; import { fetchRecipeCatalog } from "@/lib/recipes"; +import { randomUUID } from "@/lib/uuid"; import { Plus } from "lucide-react"; import { SectionDragOverlay, @@ -165,7 +166,7 @@ export function buildProfileGallerySection( position: number, ): SettingsSectionEntry { return { - id: crypto.randomUUID(), + id: randomUUID(), section_type: payload.section_type, title: payload.title, featured: payload.featured, From 0834edcfc029bd5627ed154c630956eb2e2b9646 Mon Sep 17 00:00:00 2001 From: Quick104 <31828688+Quick104@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:58:36 -0400 Subject: [PATCH 2/2] style(web): keep uuid formatting within 100-char line width Addresses CodeRabbit review on PR #458. Co-Authored-By: Claude Fable 5 --- web/src/lib/uuid.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/src/lib/uuid.ts b/web/src/lib/uuid.ts index fba9a5e3..23e64d67 100644 --- a/web/src/lib/uuid.ts +++ b/web/src/lib/uuid.ts @@ -20,5 +20,11 @@ export function randomUUID(): string { bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40; bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80; const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); - return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; + return [ + hex.slice(0, 8), + hex.slice(8, 12), + hex.slice(12, 16), + hex.slice(16, 20), + hex.slice(20), + ].join("-"); }