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..23e64d67 --- /dev/null +++ b/web/src/lib/uuid.ts @@ -0,0 +1,30 @@ +/** + * 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), + ].join("-"); +} 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,