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 <noreply@anthropic.com>
This commit is contained in:
Quick104
2026-07-23 11:51:43 -04:00
co-authored by Claude Fable 5
parent 5d7eec7a62
commit 82379fa3e2
5 changed files with 35 additions and 24 deletions
+4 -18
View File
@@ -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<string, string> {
const deviceId = getOrCreateDeviceId();
if (!deviceId) {
return {};
}
return {
"X-Silo-Device-Id": deviceId,
"X-Silo-Device-Name": detectDeviceName(),
@@ -38,6 +38,7 @@ import {
useAllUserCollections,
type CollectionOption,
} from "@/hooks/queries/useAllUserCollections";
import { randomUUID } from "@/lib/uuid";
const CATEGORY_LABELS: Record<Category, string> = {
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,
+3 -4
View File
@@ -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;
}
+24
View File
@@ -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)}`;
}
@@ -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,